mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-02-04 23:39:07 +07:00
jdk-auto: use non-modal notifications for compile actions, IDEA-253119, IDEA-240999
GitOrigin-RevId: ec7b75fef53b2261b6638b4f9034f8424f502778
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f170505ff8
commit
4635352640
@@ -677,10 +677,9 @@ public final class CompileDriver {
|
||||
if (runUnknownSdkCheck) {
|
||||
var result = CompilerDriverUnknownSdkTracker
|
||||
.getInstance(myProject)
|
||||
.fixSdkSettings(projectSdkNotSpecified, scopeModules);
|
||||
.fixSdkSettings(projectSdkNotSpecified, scopeModules, formatModulesList(modulesWithoutJdkAssigned));
|
||||
|
||||
if (result.getShouldOpenProjectStructureDialog()) {
|
||||
result.openProjectStructureDialogIfNeeded();
|
||||
if (result == CompilerDriverUnknownSdkTracker.Outcome.STOP_COMPILE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -785,13 +784,7 @@ public final class CompileDriver {
|
||||
List<String> modules,
|
||||
String editorNameToSelect) {
|
||||
String nameToSelect = notSpecifiedValueInheritedFromProject ? null : ContainerUtil.getFirstItem(modules);
|
||||
|
||||
final int maxModulesToShow = 10;
|
||||
List<String> actualNamesToInclude = new ArrayList<>(ContainerUtil.getFirstItems(modules, maxModulesToShow));
|
||||
if (modules.size() > maxModulesToShow) {
|
||||
actualNamesToInclude.add(JavaCompilerBundle.message("error.jdk.module.names.overflow.element.ellipsis"));
|
||||
}
|
||||
final String message = JavaCompilerBundle.message(resourceId, modules.size(), NlsMessages.formatNarrowAndList(actualNamesToInclude));
|
||||
final String message = JavaCompilerBundle.message(resourceId, modules.size(), formatModulesList(modules));
|
||||
|
||||
if (ApplicationManager.getApplication().isUnitTestMode()) {
|
||||
LOG.error(message);
|
||||
@@ -804,6 +797,17 @@ public final class CompileDriver {
|
||||
.showNotification();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String formatModulesList(@NotNull List<String> modules) {
|
||||
final int maxModulesToShow = 10;
|
||||
List<String> actualNamesToInclude = new ArrayList<>(ContainerUtil.getFirstItems(modules, maxModulesToShow));
|
||||
if (modules.size() > maxModulesToShow) {
|
||||
actualNamesToInclude.add(JavaCompilerBundle.message("error.jdk.module.names.overflow.element.ellipsis"));
|
||||
}
|
||||
|
||||
return NlsMessages.formatNarrowAndList(actualNamesToInclude);
|
||||
}
|
||||
|
||||
public static CompilerMessageCategory convertToCategory(CmdlineRemoteProto.Message.BuilderMessage.CompileMessage.Kind kind, CompilerMessageCategory defaultCategory) {
|
||||
switch(kind) {
|
||||
case ERROR: case INTERNAL_BUILDER_ERROR:
|
||||
|
||||
@@ -44,8 +44,14 @@ class CompileDriverNotifications(
|
||||
.setTitle(JavaCompilerBundle.message("notification.title.jps.cannot.start.compiler"))
|
||||
.setImportant(true)
|
||||
|
||||
fun withOpenSettingsAction(moduleNameToSelect: String?, tabNameToSelect: String?): LightNotification = apply {
|
||||
val handler = Runnable {
|
||||
fun withExpiringAction(@NotificationContent title : String,
|
||||
handler: () -> Unit) = apply {
|
||||
baseNotification.addAction(NotificationAction.createSimpleExpiring(title, handler))
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun withOpenSettingsAction(moduleNameToSelect: String? = null, tabNameToSelect: String? = null) =
|
||||
withExpiringAction(JavaCompilerBundle.message("notification.action.jps.open.configuration.dialog")) {
|
||||
val service = ProjectSettingsService.getInstance(project)
|
||||
if (moduleNameToSelect != null) {
|
||||
service.showModuleConfigurationDialog(moduleNameToSelect, tabNameToSelect)
|
||||
@@ -55,13 +61,7 @@ class CompileDriverNotifications(
|
||||
}
|
||||
}
|
||||
|
||||
baseNotification.addAction(NotificationAction.createSimpleExpiring(
|
||||
JavaCompilerBundle.message("notification.action.jps.open.configuration.dialog"),
|
||||
handler
|
||||
))
|
||||
}
|
||||
|
||||
fun withContent(@NotificationContent content : String): LightNotification = apply {
|
||||
fun withContent(@NotificationContent content: String): LightNotification = apply {
|
||||
baseNotification.setContent(content)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.compiler.impl
|
||||
|
||||
import com.intellij.ide.nls.NlsMessages
|
||||
import com.intellij.openapi.compiler.JavaCompilerBundle
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.ControlFlowException
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.progress.*
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.impl.UnknownSdkCollector
|
||||
import com.intellij.openapi.projectRoots.impl.UnknownSdkModalNotification
|
||||
import com.intellij.openapi.projectRoots.impl.UnknownSdkTracker
|
||||
import com.intellij.openapi.project.ProjectBundle
|
||||
import com.intellij.openapi.projectRoots.impl.*
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.util.text.HtmlBuilder
|
||||
import com.intellij.openapi.util.text.HtmlChunk
|
||||
|
||||
@Service //project
|
||||
class CompilerDriverUnknownSdkTracker(
|
||||
@@ -25,25 +28,112 @@ class CompilerDriverUnknownSdkTracker(
|
||||
}
|
||||
|
||||
fun fixSdkSettings(updateProjectSdk: Boolean,
|
||||
modules: List<Module>
|
||||
): UnknownSdkModalNotification.Outcome {
|
||||
if (!Registry.`is`("unknown.sdk.modal.jps")) return UnknownSdkModalNotification.getInstance(project).noSettingsDialogSuggested
|
||||
modules: List<Module>,
|
||||
@NlsSafe formattedModulesList: String
|
||||
): Outcome {
|
||||
if (!Registry.`is`("unknown.sdk.modal.jps")) return Outcome.CONTINUE_COMPILE
|
||||
|
||||
val collector = object: UnknownSdkCollector(project) {
|
||||
override fun checkProjectSdk(project: Project): Boolean = updateProjectSdk
|
||||
override fun collectModulesToCheckSdk(project: Project) = modules
|
||||
return ProgressManager.getInstance()
|
||||
.run(object : Task.WithResult<Outcome, Exception>(project, ProjectBundle.message("progress.title.resolving.sdks"), true) {
|
||||
override fun compute(indicator: ProgressIndicator): Outcome = try {
|
||||
computeImpl(indicator)
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
if (t is ControlFlowException) throw t
|
||||
LOG.warn("Failed to test for Unknown SDKs. ${t.message}", t)
|
||||
Outcome.CONTINUE_COMPILE
|
||||
}
|
||||
|
||||
fun computeImpl(indicator: ProgressIndicator): Outcome {
|
||||
val collector = object : UnknownSdkCollector(project) {
|
||||
override fun checkProjectSdk(project: Project): Boolean = updateProjectSdk
|
||||
override fun collectModulesToCheckSdk(project: Project) = modules
|
||||
}
|
||||
|
||||
val allActions = UnknownSdkTracker.getInstance(project).collectUnknownSdks(collector, indicator)
|
||||
if (allActions.isEmpty()) return Outcome.CONTINUE_COMPILE
|
||||
|
||||
val actions = UnknownSdkTracker
|
||||
.getInstance(project)
|
||||
.applyAutoFixesAndNotify(allActions, indicator)
|
||||
|
||||
if (actions.isEmpty()) return Outcome.CONTINUE_COMPILE
|
||||
return processManualFixes(modules, actions)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enum class Outcome {
|
||||
//we were able to apply a fix (probably not enough fixes)
|
||||
STOP_COMPILE,
|
||||
|
||||
//we see no reasons to stop the standard logic
|
||||
CONTINUE_COMPILE,
|
||||
}
|
||||
|
||||
fun processManualFixes(modules: List<Module>,
|
||||
actions: List<UnknownSdkFix>): Outcome {
|
||||
val actionsWithFix = actions.mapNotNull { it.suggestedFixAction }
|
||||
val actionsWithoutFix = actions.filter { it.suggestedFixAction == null }
|
||||
|
||||
if (actionsWithFix.isEmpty()) {
|
||||
//nothing to do. We fallback to the default behaviour because there is nothing we can do better
|
||||
return Outcome.CONTINUE_COMPILE
|
||||
}
|
||||
|
||||
val moduleNames = modules.map { JavaCompilerBundle.message("dialog.message.error.jdk.not.specified.with.module.name.quoted", it.name) }.toSortedSet()
|
||||
val errorMessage = JavaCompilerBundle.message("dialog.title.error.jdk.not.specified.with.fixSuggestion")
|
||||
val errorText = JavaCompilerBundle.message("dialog.message.error.jdk.not.specified.with.fixSuggestion", moduleNames.size, NlsMessages.formatAndList(moduleNames))
|
||||
val message = HtmlBuilder()
|
||||
|
||||
val handler = UnknownSdkModalNotification
|
||||
message.append(JavaCompilerBundle.message(
|
||||
"dialog.message.error.jdk.not.specified.with.fixSuggestion",
|
||||
modules.size,
|
||||
modules.size)
|
||||
)
|
||||
|
||||
message.append(HtmlChunk.ul().children(actionsWithFix.sortedBy { it.actionDetailedText.toLowerCase() }.map { fix ->
|
||||
var li = HtmlChunk.li().addText(fix.actionDetailedText)
|
||||
fix.actionTooltipText?.let {
|
||||
li = li.addText(it)
|
||||
}
|
||||
li
|
||||
}))
|
||||
|
||||
if (actionsWithoutFix.isNotEmpty()) {
|
||||
message.append(JavaCompilerBundle.message("dialog.message.error.jdk.not.specified.with.noFix"))
|
||||
message.append(HtmlChunk.ul().children(actionsWithoutFix.sortedBy { it.notificationText.toLowerCase() }.map { fix ->
|
||||
HtmlChunk.li().addText(fix.notificationText)
|
||||
}))
|
||||
}
|
||||
|
||||
CompileDriverNotifications
|
||||
.getInstance(project)
|
||||
.newModalHandler(errorMessage, errorText)
|
||||
.createCannotStartNotification()
|
||||
.withContent(message.toString())
|
||||
.withExpiringAction(JavaCompilerBundle.message("dialog.message.action.apply.fix")) { applySuggestions(actionsWithFix) }
|
||||
.withOpenSettingsAction(modules.firstOrNull()?.name, null)
|
||||
.showNotification()
|
||||
|
||||
UnknownSdkTracker.getInstance(project).collectUnknownSdksBlocking(collector, handler)
|
||||
return Outcome.STOP_COMPILE
|
||||
}
|
||||
|
||||
return handler.outcome
|
||||
private fun applySuggestions(suggestions: List<UnknownSdkFixAction>) {
|
||||
if (suggestions.isEmpty()) return
|
||||
|
||||
val task = object : Task.Backgroundable(project, ProjectBundle.message("progress.title.resolving.sdks"), true) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
for (suggestion in suggestions) {
|
||||
try {
|
||||
indicator.withPushPop {
|
||||
suggestion.applySuggestionBlocking(indicator)
|
||||
}
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
if (t is ControlFlowException) break
|
||||
LOG.warn("Failed to apply suggestion $suggestion. ${t.message}", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProgressManager.getInstance().run(task)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ java.compiler.description=Java Compiler
|
||||
rmi.compiler.description=RMI Compiler
|
||||
error.jdk.not.specified=The SDK is not specified for {0,choice, 1#module|2#modules\n} {1}
|
||||
error.jdk.module.names.overflow.element.ellipsis=...
|
||||
dialog.title.error.jdk.not.specified.with.fixSuggestion=Cannot Start Compiler
|
||||
dialog.message.error.jdk.not.specified.with.module.name.quoted="{0}"
|
||||
dialog.message.error.jdk.not.specified.with.fixSuggestion=The SDK is not specified for {1} {0,choice, 1#module|2#modules}
|
||||
dialog.message.error.jdk.not.specified.with.fixSuggestion=SDK is not specified or corrupted<br/>for {0} {1,choice, 1#module|2#modules}. Resolve the following SDKs automatically?
|
||||
dialog.message.error.jdk.not.specified.with.noFix=Manual configuration is still required for:
|
||||
dialog.message.action.apply.fix=Fix automatically
|
||||
error.output.not.specified=The output path is not specified for {0,choice, 1#module|2#modules\n} {1}
|
||||
compiler.javac.name=Javac
|
||||
compiler.configurable.display.name=Compiler
|
||||
|
||||
@@ -36,8 +36,7 @@ public interface UnknownSdkFixAction {
|
||||
void applySuggestionAsync(@Nullable Project project);
|
||||
|
||||
/**
|
||||
* Applies suggestions under a modal progress, e.g. as a part of
|
||||
* the {@link UnknownSdkModalNotification}.
|
||||
* Applies suggestions under a given progress
|
||||
*/
|
||||
void applySuggestionBlocking(@NotNull ProgressIndicator indicator);
|
||||
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.openapi.application.invokeAndWaitIfNeeded
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.ControlFlowException
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.withPushPop
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectBundle
|
||||
import com.intellij.openapi.projectRoots.impl.UnknownSdkTracker.ShowStatusCallback
|
||||
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.ui.layout.*
|
||||
import org.jetbrains.annotations.Nls
|
||||
import javax.swing.Action
|
||||
|
||||
@Service
|
||||
class UnknownSdkModalNotification(
|
||||
private val project: Project
|
||||
) {
|
||||
companion object {
|
||||
private val LOG = logger<UnknownSdkModalNotification>()
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(project: Project) = project.service<UnknownSdkModalNotification>()
|
||||
}
|
||||
|
||||
interface Outcome {
|
||||
val shouldOpenProjectStructureDialog: Boolean
|
||||
fun openProjectStructureDialogIfNeeded()
|
||||
}
|
||||
|
||||
val noSettingsDialogSuggested = object: Outcome {
|
||||
override val shouldOpenProjectStructureDialog: Boolean = false
|
||||
override fun openProjectStructureDialogIfNeeded() {}
|
||||
}
|
||||
|
||||
private val OPEN_DIALOG = object : Outcome {
|
||||
override val shouldOpenProjectStructureDialog: Boolean = true
|
||||
|
||||
override fun openProjectStructureDialogIfNeeded() {
|
||||
if (!shouldOpenProjectStructureDialog) return
|
||||
val service = ProjectSettingsService.getInstance(project)
|
||||
service.openProjectSettings()
|
||||
}
|
||||
}
|
||||
|
||||
interface UnknownSdkModalNotificationHandler : ShowStatusCallback {
|
||||
val outcome: Outcome
|
||||
}
|
||||
|
||||
fun newModalHandler(@Nls dialogTitle: String,
|
||||
@Nls detailedMessage: String?
|
||||
): UnknownSdkModalNotificationHandler = object : ShowStatusCallback, UnknownSdkModalNotificationHandler {
|
||||
override var outcome: Outcome = noSettingsDialogSuggested
|
||||
|
||||
override fun showStatus(allActions: List<UnknownSdkFix>, indicator: ProgressIndicator) {
|
||||
outcome = kotlin.runCatching { showStatusImpl(dialogTitle, detailedMessage, allActions, indicator) }.getOrElse { noSettingsDialogSuggested }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showStatusImpl(@Nls dialogTitle: String,
|
||||
@Nls detailedMessage: String?,
|
||||
allActions: List<UnknownSdkFix>,
|
||||
indicator: ProgressIndicator) : Outcome {
|
||||
if (allActions.isEmpty()) return noSettingsDialogSuggested
|
||||
|
||||
val actions = UnknownSdkTracker
|
||||
.getInstance(project)
|
||||
.applyAutoFixesAndNotify(allActions, indicator)
|
||||
|
||||
val actionsWithFix = actions.mapNotNull { it.suggestedFixAction }
|
||||
val actionsWithoutFix = actions.filter { it.suggestedFixAction == null }
|
||||
|
||||
if (actionsWithFix.isEmpty()) {
|
||||
//nothing to do. We fallback to the default behaviour because there is nothing we can do better
|
||||
return noSettingsDialogSuggested
|
||||
}
|
||||
|
||||
val isOk = invokeAndWaitIfNeeded {
|
||||
createConfirmSdkDownloadFixDialog(dialogTitle, detailedMessage, actionsWithFix, actionsWithoutFix).showAndGet()
|
||||
}
|
||||
|
||||
if (isOk) {
|
||||
val suggestionsApplied = applySuggestions(actionsWithFix, indicator)
|
||||
if (!suggestionsApplied) return OPEN_DIALOG
|
||||
}
|
||||
|
||||
if (actionsWithoutFix.isNotEmpty()) {
|
||||
return OPEN_DIALOG
|
||||
}
|
||||
|
||||
return noSettingsDialogSuggested
|
||||
}
|
||||
|
||||
private fun createConfirmSdkDownloadFixDialog(
|
||||
@Nls dialogTitle: String,
|
||||
@Nls detailedMessage: String?,
|
||||
actions: List<UnknownSdkFixAction>,
|
||||
actionsWithoutFix: List<UnknownSdkFix>
|
||||
) = object : DialogWrapper(project) {
|
||||
init {
|
||||
require(actions.isNotEmpty()) { "There must be fix suggestions! " }
|
||||
|
||||
title = dialogTitle
|
||||
setResizable(false)
|
||||
init()
|
||||
|
||||
val okMessage = when {
|
||||
actionsWithoutFix.isEmpty() -> ProjectBundle.message("dialog.button.download.sdks")
|
||||
else -> ProjectBundle.message("dialog.button.download.sdksAndOpenDialog")
|
||||
}
|
||||
|
||||
myOKAction.putValue(Action.NAME, okMessage)
|
||||
myCancelAction.putValue(Action.NAME, ProjectBundle.message("dialog.button.open.settings"))
|
||||
}
|
||||
|
||||
override fun createCenterPanel() = panel {
|
||||
detailedMessage?.let {
|
||||
row {
|
||||
label(it)
|
||||
}
|
||||
}
|
||||
|
||||
row {
|
||||
label(ProjectBundle.message("dialog.text.resolving.sdks.suggestions"))
|
||||
}
|
||||
|
||||
if (actions.isNotEmpty()) {
|
||||
actions.sortedBy { it.actionDetailedText.toLowerCase() }.forEach {
|
||||
row(ProjectBundle.message("dialog.section.bullet")) {
|
||||
val label = label(it.actionDetailedText)
|
||||
it.actionTooltipText?.let {
|
||||
label.comment(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actionsWithoutFix.isNotEmpty()) {
|
||||
row {
|
||||
label(ProjectBundle.message("dialog.text.resolving.sdks.unknowns"))
|
||||
}
|
||||
|
||||
actionsWithoutFix.sortedBy { it.notificationText.toLowerCase() }.forEach {
|
||||
row(ProjectBundle.message("dialog.section.bullet")) {
|
||||
label(it.notificationText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applySuggestions(suggestions: List<UnknownSdkFixAction>, indicator: ProgressIndicator): Boolean {
|
||||
if (suggestions.isEmpty()) return true
|
||||
|
||||
var isFailed = false
|
||||
for (suggestion in suggestions) {
|
||||
try {
|
||||
indicator.withPushPop {
|
||||
suggestion.applySuggestionBlocking(indicator)
|
||||
}
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
isFailed = true
|
||||
if (t is ControlFlowException) break
|
||||
LOG.warn("Failed to apply suggestion $suggestion. ${t.message}", t)
|
||||
}
|
||||
}
|
||||
return !isFailed
|
||||
}
|
||||
}
|
||||
@@ -57,27 +57,6 @@ public class UnknownSdkTracker {
|
||||
return Registry.is("unknown.sdk") && UnknownSdkResolver.EP_NAME.hasAnyExtensions();
|
||||
}
|
||||
|
||||
public void collectUnknownSdksBlocking(@NotNull UnknownSdkBlockingCollector collector,
|
||||
@NotNull ShowStatusCallback showStatus) {
|
||||
if (!isEnabled()) {
|
||||
showStatus.showEmptyStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
ProgressManager.getInstance()
|
||||
.run(new Task.Modal(myProject, ProjectBundle.message("progress.title.resolving.sdks"), true) {
|
||||
@Override
|
||||
public void run(@NotNull ProgressIndicator indicator) {
|
||||
var snapshot = collector.collectSdksBlocking();
|
||||
|
||||
var action = createProcessSdksAction(snapshot, showStatus);
|
||||
if (action == null) return;
|
||||
|
||||
action.run(indicator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public @NotNull List<UnknownSdkFix> collectUnknownSdks(@NotNull UnknownSdkBlockingCollector collector,
|
||||
@NotNull ProgressIndicator indicator) {
|
||||
if (!isEnabled()) {
|
||||
|
||||
@@ -270,13 +270,7 @@ notification.text.jdk.update.found=Update JDK "{0}" to {1}. The current version
|
||||
notification.link.jdk.update.apply=Download
|
||||
notification.link.jdk.update.skip=Skip this update
|
||||
notification.link.jdk.update.retry=Retry
|
||||
dialog.text.resolving.sdks.suggestions=Resolve the following SDKs automatically?
|
||||
dialog.text.resolving.sdks.item={0} "{1}"
|
||||
dialog.text.resolving.sdks.unknowns=Manual configuration is still required, the Project Structure dialog will be opened for:
|
||||
dialog.button.download.sdks=Fix
|
||||
dialog.button.download.sdksAndOpenDialog=Apply Suggestions...
|
||||
dialog.button.open.settings=Open Project Structure Dialog...
|
||||
dialog.section.bullet=\u2022
|
||||
unknown.sdk.with.no.name=<unknown>
|
||||
list.item.all.frameworks=All Frameworks...
|
||||
#0 - file name
|
||||
|
||||
Reference in New Issue
Block a user