diff --git a/build.gradle.kts b/build.gradle.kts index 3266d24e9..d92ec38e5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ import java.time.format.DateTimeFormatter // Specify UTF-8 for all compilations so we avoid Windows-1252. allprojects { tasks.withType { + options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation")) options.encoding = "UTF-8" } tasks.withType { @@ -316,6 +317,18 @@ intellijPlatform { } } else { recommended() + select { + types = listOf(IntelliJPlatformType.AndroidStudio) + channels = listOf(ProductRelease.Channel.RELEASE) + sinceBuild = "2024.1" + untilBuild = "2025.2.*" + } + select { + types = listOf(IntelliJPlatformType.IntellijIdeaCommunity, IntelliJPlatformType.IntellijIdeaUltimate) + channels = listOf(ProductRelease.Channel.RELEASE) + sinceBuild = "243" + untilBuild = "253.*" + } } } } diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 2e9155ad6..c95a8f5ba 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -283,6 +283,7 @@ + diff --git a/src/io/flutter/FlutterInitializer.java b/src/io/flutter/FlutterInitializer.java index 0e9302e4b..401b00d71 100644 --- a/src/io/flutter/FlutterInitializer.java +++ b/src/io/flutter/FlutterInitializer.java @@ -25,6 +25,7 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.project.ModuleListener; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.concurrency.AppExecutorUtil; @@ -173,9 +174,9 @@ public void modulesAdded(@NotNull Project project, @NotNull List modules = FlutterModuleUtils.findModulesWithFlutterContents(project); for (Module module : modules) { if (module.isDisposed() || !FlutterModuleUtils.isFlutterModule(module)) continue; - VirtualFile moduleFile = module.getModuleFile(); - if (moduleFile == null) continue; - VirtualFile baseDir = moduleFile.getParent(); + VirtualFile baseDir = ProjectUtil.guessModuleDir(module); + if (baseDir == null) + continue; if (baseDir.getName().equals(".idea")) { baseDir = baseDir.getParent(); } diff --git a/src/io/flutter/actions/FlutterBuildActionGroup.java b/src/io/flutter/actions/FlutterBuildActionGroup.java index ee4ab490b..a4aa3df39 100644 --- a/src/io/flutter/actions/FlutterBuildActionGroup.java +++ b/src/io/flutter/actions/FlutterBuildActionGroup.java @@ -6,8 +6,8 @@ package io.flutter.actions; import com.intellij.execution.process.ColoredProcessHandler; -import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.process.ProcessListener; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; @@ -33,7 +33,7 @@ public static void build(@NotNull Project project, @Nullable String desc) { final ProgressHelper progressHelper = new ProgressHelper(project); progressHelper.start(desc == null ? "building" : desc); - ProcessAdapter processAdapter = new ProcessAdapter() { + ProcessListener processListener = new ProcessListener() { @Override public void processTerminated(@NotNull ProcessEvent event) { progressHelper.done(); @@ -42,10 +42,22 @@ public void processTerminated(@NotNull ProcessEvent event) { FlutterMessages.showError("Error while building " + buildType, "`flutter build` returned: " + exitCode, project); } } + + @Override + public void startNotified(@NotNull ProcessEvent event) { + } + + @Override + public void processWillTerminate(@NotNull ProcessEvent event, boolean willBeDestroyed) { + } + + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull com.intellij.openapi.util.Key outputType) { + } }; final Module module = pubRoot.getModule(project); if (module != null) { - sdk.flutterBuild(pubRoot, buildType.type).startInModuleConsole(module, pubRoot::refresh, processAdapter); + sdk.flutterBuild(pubRoot, buildType.type).startInModuleConsole(module, pubRoot::refresh, processListener); } else { final ColoredProcessHandler processHandler = sdk.flutterBuild(pubRoot, buildType.type).startInConsole(project); @@ -53,7 +65,7 @@ public void processTerminated(@NotNull ProcessEvent event) { progressHelper.done(); } else { - processHandler.addProcessListener(processAdapter); + processHandler.addProcessListener(processListener); } } } diff --git a/src/io/flutter/actions/FlutterNewProjectAction.kt b/src/io/flutter/actions/FlutterNewProjectAction.kt index f8d905214..3e7f7ed47 100644 --- a/src/io/flutter/actions/FlutterNewProjectAction.kt +++ b/src/io/flutter/actions/FlutterNewProjectAction.kt @@ -5,7 +5,7 @@ */ package io.flutter.actions -import com.intellij.ide.impl.createNewProjectAsync + import com.intellij.ide.projectWizard.NewProjectWizard import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -41,7 +41,9 @@ class FlutterNewProjectAction : AnAction(), DumbAware { val wizard = withContext(Dispatchers.EDT) { NewProjectWizard(null, ModulesProvider.EMPTY_MODULES_PROVIDER, null) } - createNewProjectAsync(wizard) + com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater { + com.intellij.ide.impl.NewProjectUtil.createNewProject(wizard) + } } } diff --git a/src/io/flutter/actions/FlutterRetargetAppAction.java b/src/io/flutter/actions/FlutterRetargetAppAction.java index 554bfa407..9a08adc03 100644 --- a/src/io/flutter/actions/FlutterRetargetAppAction.java +++ b/src/io/flutter/actions/FlutterRetargetAppAction.java @@ -72,7 +72,8 @@ public void update(@NotNull AnActionEvent e) { if (text != null) { presentation.setText(text, true); } - action.update(e); + // action.update(e) is override-only. + com.intellij.openapi.actionSystem.ex.ActionUtil.performDumbAwareUpdate(action, e, false); } } diff --git a/src/io/flutter/actions/OpenInAndroidStudioAction.java b/src/io/flutter/actions/OpenInAndroidStudioAction.java index f3c7446a4..d97f9fb5b 100644 --- a/src/io/flutter/actions/OpenInAndroidStudioAction.java +++ b/src/io/flutter/actions/OpenInAndroidStudioAction.java @@ -8,7 +8,8 @@ import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.ColoredProcessHandler; -import com.intellij.execution.process.ProcessAdapter; + +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.process.ProcessEvent; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.editor.CaretModel; @@ -245,7 +246,7 @@ private static void openFileInStudio(@NotNull String androidStudioPath, cmd.addParameter(sourceFile); } final ColoredProcessHandler handler = new ColoredProcessHandler(cmd); - handler.addProcessListener(new ProcessAdapter() { + handler.addProcessListener(new ProcessListener() { @Override public void processTerminated(@NotNull final ProcessEvent event) { if (event.getExitCode() != 0) { diff --git a/src/io/flutter/actions/OpenInAppCodeAction.java b/src/io/flutter/actions/OpenInAppCodeAction.java index b32e34426..fb849bece 100644 --- a/src/io/flutter/actions/OpenInAppCodeAction.java +++ b/src/io/flutter/actions/OpenInAppCodeAction.java @@ -8,7 +8,7 @@ import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.ColoredProcessHandler; -import com.intellij.execution.process.ProcessAdapter; +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.openapi.actionSystem.ActionUpdateThread; @@ -46,13 +46,25 @@ private static void initialize() { final GeneralCommandLine cmd = new GeneralCommandLine().withExePath("/bin/bash") .withParameters("-c", "mdfind \"kMDItemContentType == 'com.apple.application-bundle'\" | grep AppCode.app"); final ColoredProcessHandler handler = new ColoredProcessHandler(cmd); - handler.addProcessListener(new ProcessAdapter() { + handler.addProcessListener(new ProcessListener() { @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { if (outputType == ProcessOutputTypes.STDOUT) { IS_APPCODE_INSTALLED = true; } } + + @Override + public void processTerminated(@NotNull ProcessEvent event) { + } + + @Override + public void startNotified(@NotNull ProcessEvent event) { + } + + @Override + public void processWillTerminate(@NotNull ProcessEvent event, boolean willBeDestroyed) { + } }); handler.startNotify(); } @@ -110,13 +122,25 @@ private static void openInAppCode(@Nullable Project project, @NotNull String pat try { final GeneralCommandLine cmd = new GeneralCommandLine().withExePath("open").withParameters("-a", "AppCode.app", path); final ColoredProcessHandler handler = new ColoredProcessHandler(cmd); - handler.addProcessListener(new ProcessAdapter() { + handler.addProcessListener(new ProcessListener() { @Override public void processTerminated(@NotNull final ProcessEvent event) { if (event.getExitCode() != 0) { FlutterMessages.showError("Error Opening", path, project); } } + + @Override + public void startNotified(@NotNull ProcessEvent event) { + } + + @Override + public void processWillTerminate(@NotNull ProcessEvent event, boolean willBeDestroyed) { + } + + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull com.intellij.openapi.util.Key outputType) { + } }); handler.startNotify(); } diff --git a/src/io/flutter/actions/OpenInXcodeAction.java b/src/io/flutter/actions/OpenInXcodeAction.java index ca96e2e66..09eb93674 100644 --- a/src/io/flutter/actions/OpenInXcodeAction.java +++ b/src/io/flutter/actions/OpenInXcodeAction.java @@ -8,11 +8,12 @@ import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.ColoredProcessHandler; -import com.intellij.execution.process.ProcessAdapter; +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.process.ProcessEvent; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.util.Key; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.vfs.VirtualFile; import io.flutter.FlutterMessages; @@ -81,7 +82,7 @@ private static void openFile(@NotNull VirtualFile file) { FlutterMessages.showError("Error Opening Xcode", "unable to run `flutter build`", project); } else { - processHandler.addProcessListener(new ProcessAdapter() { + processHandler.addProcessListener(new ProcessListener() { @Override public void processTerminated(@NotNull ProcessEvent event) { progressHelper.done(); @@ -94,6 +95,18 @@ public void processTerminated(@NotNull ProcessEvent event) { openWithXcode(project, file.getPath()); } + + @Override + public void startNotified(@NotNull ProcessEvent event) { + } + + @Override + public void processWillTerminate(@NotNull ProcessEvent event, boolean willBeDestroyed) { + } + + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull com.intellij.openapi.util.Key outputType) { + } }); } } @@ -117,13 +130,25 @@ private static void openWithXcode(@Nullable Project project, @NotNull String pat try { final GeneralCommandLine cmd = new GeneralCommandLine().withExePath("open").withParameters(path); final ColoredProcessHandler handler = new ColoredProcessHandler(cmd); - handler.addProcessListener(new ProcessAdapter() { + handler.addProcessListener(new ProcessListener() { @Override public void processTerminated(@NotNull final ProcessEvent event) { if (event.getExitCode() != 0) { FlutterMessages.showError("Error Opening", path, project); } } + + @Override + public void startNotified(@NotNull ProcessEvent event) { + } + + @Override + public void processWillTerminate(@NotNull ProcessEvent event, boolean willBeDestroyed) { + } + + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull com.intellij.openapi.util.Key outputType) { + } }); handler.startNotify(); } diff --git a/src/io/flutter/actions/ReloadFlutterApp.java b/src/io/flutter/actions/ReloadFlutterApp.java index c3b3f47ff..6a983b234 100644 --- a/src/io/flutter/actions/ReloadFlutterApp.java +++ b/src/io/flutter/actions/ReloadFlutterApp.java @@ -49,7 +49,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { // If the shift key is held down, perform a restart. We check to see if we're being invoked from the // 'GoToAction' dialog. If so, the modifiers are for the command that opened the go-to action dialog. - final boolean shouldRestart = (e.getModifiers() & InputEvent.SHIFT_MASK) != 0 && !"GoToAction".equals(e.getPlace()); + final boolean shouldRestart = (e.getModifiers() & InputEvent.SHIFT_DOWN_MASK) != 0 + && !"GoToAction".equals(e.getPlace()); analyticsData.add(AnalyticsConstants.REQUIRES_RESTART, shouldRestart); var reloadManager = FlutterReloadManager.getInstance(project); diff --git a/src/io/flutter/android/AndroidSdk.java b/src/io/flutter/android/AndroidSdk.java index 9a5d2b155..0bcc77741 100644 --- a/src/io/flutter/android/AndroidSdk.java +++ b/src/io/flutter/android/AndroidSdk.java @@ -8,8 +8,9 @@ import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.ColoredProcessHandler; -import com.intellij.execution.process.ProcessAdapter; + import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; @@ -95,7 +96,7 @@ public List getEmulators() { try { final StringBuilder stringBuilder = new StringBuilder(); final ColoredProcessHandler process = new ColoredProcessHandler(cmd); - process.addProcessListener(new ProcessAdapter() { + process.addProcessListener(new ProcessListener() { @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { if (outputType == ProcessOutputTypes.STDOUT) { diff --git a/src/io/flutter/console/FlutterConsole.java b/src/io/flutter/console/FlutterConsole.java index 1cc597e8f..99cfa7f10 100644 --- a/src/io/flutter/console/FlutterConsole.java +++ b/src/io/flutter/console/FlutterConsole.java @@ -8,7 +8,8 @@ import com.intellij.execution.filters.TextConsoleBuilder; import com.intellij.execution.filters.TextConsoleBuilderFactory; import com.intellij.execution.process.ColoredProcessHandler; -import com.intellij.execution.process.ProcessAdapter; + +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.ui.ConsoleView; import com.intellij.execution.ui.ConsoleViewContentType; @@ -81,7 +82,7 @@ void watchProcess(@NotNull ColoredProcessHandler process) { view.attachToProcess(process); // Print exit code. - final ProcessAdapter listener = new ProcessAdapter() { + final ProcessListener listener = new ProcessListener() { @Override public void processTerminated(final @NotNull ProcessEvent event) { view.print( diff --git a/src/io/flutter/console/FlutterConsoleFilter.java b/src/io/flutter/console/FlutterConsoleFilter.java index 963941dd8..85e84ba2f 100644 --- a/src/io/flutter/console/FlutterConsoleFilter.java +++ b/src/io/flutter/console/FlutterConsoleFilter.java @@ -12,10 +12,12 @@ import com.intellij.execution.filters.HyperlinkInfo; import com.intellij.execution.filters.OpenFileHyperlinkInfo; import com.intellij.execution.process.ColoredProcessHandler; -import com.intellij.execution.process.ProcessAdapter; +import com.intellij.execution.process.ProcessListener; +import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.execution.process.ProcessEvent; import com.intellij.openapi.editor.markup.EffectType; import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.util.Key; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; @@ -54,13 +56,25 @@ public void navigate(@NotNull Project project) { try { final GeneralCommandLine cmd = new GeneralCommandLine().withExePath("open").withParameters(myPath); final ColoredProcessHandler handler = new ColoredProcessHandler(cmd); - handler.addProcessListener(new ProcessAdapter() { + handler.addProcessListener(new ProcessListener() { + @Override + public void startNotified(@NotNull ProcessEvent event) { + } + @Override public void processTerminated(@NotNull final ProcessEvent event) { if (event.getExitCode() != 0) { FlutterMessages.showError("Error Opening ", myPath, project); } } + + @Override + public void processWillTerminate(@NotNull ProcessEvent event, boolean willBeDestroyed) { + } + + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { + } }); handler.startNotify(); } diff --git a/src/io/flutter/editor/NativeEditorNotificationProvider.java b/src/io/flutter/editor/NativeEditorNotificationProvider.java index b20b86c6d..df2f26edd 100644 --- a/src/io/flutter/editor/NativeEditorNotificationProvider.java +++ b/src/io/flutter/editor/NativeEditorNotificationProvider.java @@ -107,6 +107,7 @@ class NativeEditorActionsPanel extends EditorNotificationPanel { final AnAction myAction; final boolean isVisible; + @SuppressWarnings("deprecation") NativeEditorActionsPanel(@NotNull FileEditor fileEditor, @NotNull VirtualFile root, @NotNull String actionName) { super(UIUtils.getEditorNotificationBackgroundColor()); myFile = fileEditor.getFile(); @@ -117,8 +118,9 @@ class NativeEditorActionsPanel extends EditorNotificationPanel { text("Flutter commands"); // Ensure this project is a Flutter project by updating the menu action. It will only be visible for Flutter projects. - myAction.update( - AnActionEvent.createEvent(makeContext(), myAction.getTemplatePresentation(), ActionPlaces.EDITOR_TOOLBAR, ActionUiKind.NONE, null)); + final AnActionEvent event = new AnActionEvent(null, makeContext(), + ActionPlaces.EDITOR_TOOLBAR, myAction.getTemplatePresentation(), ActionManager.getInstance(), 0); + com.intellij.openapi.actionSystem.ex.ActionUtil.performDumbAwareUpdate(myAction, event, false); isVisible = myAction.getTemplatePresentation().isVisible(); //noinspection DialogTitleCapitalization @@ -141,7 +143,8 @@ private boolean isValidForFile() { private void performAction() { // Open Xcode or Android Studio. If already running AS then just open a new window. myAction.actionPerformed( - AnActionEvent.createEvent(makeContext(), myAction.getTemplatePresentation(), ActionPlaces.EDITOR_TOOLBAR, ActionUiKind.NONE, null)); + new AnActionEvent(null, makeContext(), ActionPlaces.EDITOR_TOOLBAR, myAction.getTemplatePresentation(), + ActionManager.getInstance(), 0)); } private DataContext makeContext() { diff --git a/src/io/flutter/module/FlutterGeneratorPeer.java b/src/io/flutter/module/FlutterGeneratorPeer.java index a80c6961c..9eece873a 100644 --- a/src/io/flutter/module/FlutterGeneratorPeer.java +++ b/src/io/flutter/module/FlutterGeneratorPeer.java @@ -75,8 +75,8 @@ private void init() { } } - mySdkPathComboWithBrowse.addBrowseFolderListener(null, FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(FlutterBundle.message("flutter.sdk.browse.path.label")), TextComponentAccessor.STRING_COMBOBOX_WHOLE_TEXT); + mySdkPathComboWithBrowse.addBrowseFolderListener(FlutterBundle.message("flutter.sdk.browse.path.label"), null, null, + FileChooserDescriptorFactory.createSingleFolderDescriptor(), TextComponentAccessor.STRING_COMBOBOX_WHOLE_TEXT); mySdkPathComboWithBrowse.getComboBox().addActionListener(e -> fillSdkCache()); fillSdkCache(); diff --git a/src/io/flutter/project/FlutterAndroidModulePostStartupActivity.kt b/src/io/flutter/project/FlutterAndroidModulePostStartupActivity.kt new file mode 100644 index 000000000..2995af5de --- /dev/null +++ b/src/io/flutter/project/FlutterAndroidModulePostStartupActivity.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2025 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +package io.flutter.project + +import com.android.tools.idea.npw.importing.SourceToGradleModuleStep +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.startup.StartupManager +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import io.flutter.module.FlutterModuleBuilder +import io.flutter.utils.FlutterModuleUtils +import io.flutter.utils.OpenApiUtils +import java.io.File + +class FlutterAndroidModulePostStartupActivity : ProjectActivity { + override suspend fun execute(project: Project) { + if (ApplicationManager.getApplication().isUnitTestMode) return + + DumbService.getInstance(project).smartInvokeLater { + addAndroidModuleIfNeeded(project) + } + } + + private fun addAndroidModuleIfNeeded(project: Project) { + val modules = FlutterModuleUtils.getModules(project) + for (module in modules) { + if (!FlutterModuleUtils.declaresFlutter(module)) continue + + val roots = OpenApiUtils.getContentRoots(module) + if (roots.isEmpty() || roots.size > 1) continue + + val root = roots[0] + val moduleName = module.name + + // Check if Android module logic applies + if (!projectHasContentRoot(project, root)) continue + + val imlName = "${moduleName}_android.iml" + var moduleDir: File? = null + + val rootFile = File(root.path) + if (File(rootFile, imlName).exists()) { + moduleDir = rootFile + } else { + for (name in arrayOf("android", ".android")) { + val dir = File(rootFile, name) + if (dir.exists() && File(dir, imlName).exists()) { + moduleDir = dir + break + } + } + } + + if (moduleDir != null) { + try { + FlutterModuleBuilder.addAndroidModule(project, null, moduleDir.path, module.name, true) + } catch (ignored: IllegalStateException) { + } + } + + // Check for example module + val example = File(rootFile, "example") + if (example.exists()) { + val android = File(example, "android") + val exampleFile = File(android, "${moduleName}_example_android.iml") + if (android.exists() && exampleFile.exists()) { + try { + val virtualExampleFile = LocalFileSystem.getInstance().findFileByIoFile(exampleFile) + if (virtualExampleFile != null) { + FlutterModuleBuilder.addAndroidModuleFromFile(project, null, virtualExampleFile) + } + } catch (ignored: IllegalStateException) { + } + } + } + } + } + + private fun projectHasContentRoot(project: Project, root: VirtualFile): Boolean { + for (module in FlutterModuleUtils.getModules(project)) { + for (file in OpenApiUtils.getContentRoots(module)) { + if (root == file) return true + } + } + return false + } + + companion object { + private val LOG = Logger.getInstance(FlutterAndroidModulePostStartupActivity::class.java) + } +} diff --git a/src/io/flutter/project/FlutterProjectStructureDetector.java b/src/io/flutter/project/FlutterProjectStructureDetector.java index b88354a1a..be8f0c830 100644 --- a/src/io/flutter/project/FlutterProjectStructureDetector.java +++ b/src/io/flutter/project/FlutterProjectStructureDetector.java @@ -10,18 +10,11 @@ import com.intellij.ide.util.projectWizard.importSources.DetectedProjectRoot; import com.intellij.ide.util.projectWizard.importSources.ProjectFromSourcesBuilder; import com.intellij.ide.util.projectWizard.importSources.ProjectStructureDetector; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.module.Module; -import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.project.ProjectManagerListener; -import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.util.messages.MessageBusConnection; -import io.flutter.module.FlutterModuleBuilder; import io.flutter.pub.PubRoot; import io.flutter.utils.FlutterModuleUtils; import io.flutter.utils.OpenApiUtils; @@ -74,110 +67,6 @@ public void setupProjectStructure(@NotNull Collection roots projectDescriptor.setModules(modules); builder.setupModulesByContentRoots(projectDescriptor, roots); - String name = builder.getContext().getProjectName(); - if (name != null) { - scheduleAndroidModuleAddition(name, modules, 0); - } - } - - private void scheduleAndroidModuleAddition(@NotNull String projectName, @NotNull List modules, int tries) { - //noinspection ConstantConditions - final MessageBusConnection[] connection = {ApplicationManager.getApplication().getMessageBus().connect()}; - scheduleDisconnectIfCancelled(connection); - //noinspection ConstantConditions - connection[0].subscribe(ProjectManager.TOPIC, new ProjectManagerListener() { - //See https://plugins.jetbrains.com/docs/intellij/plugin-components.html#comintellijpoststartupactivity - // for notice and documentation on the deprecation intentions of - // Components from JetBrains. - // - // Migration forward has different directions before and after - // 2023.1, if we can, it would be prudent to wait until we are - // only supporting this major platform as a minimum version. - // - // https://github.com/flutter/flutter-intellij/issues/6953 - @Override - public void projectOpened(@NotNull Project project) { - if (connection[0] != null) { - connection[0].disconnect(); - connection[0] = null; - } - if (!projectName.equals(project.getName())) { - // This can happen if you have selected project roots in the import wizard then cancel the import, - // and then import a project with a different name, before the scheduled disconnect runs. - return; - } - //noinspection ConstantConditions - StartupManager.getInstance(project).runAfterOpened(() -> { - DumbService.getInstance(project).smartInvokeLater(() -> { - for (ModuleDescriptor module : modules) { - assert module != null; - Set roots = module.getContentRoots(); - String moduleName = module.getName(); - if (roots == null || roots.size() != 1 || moduleName == null) continue; - File root = roots.iterator().next(); - assert root != null; - if (!projectHasContentRoot(project, root)) continue; - String imlName = moduleName + "_android.iml"; - File moduleDir = null; - if (new File(root, imlName).exists()) { - moduleDir = root; - } - else { - for (String name : new String[]{"android", ".android"}) { - File dir = new File(root, name); - if (dir.exists() && new File(dir, imlName).exists()) { - moduleDir = dir; - break; - } - } - } - if (moduleDir == null) continue; - try { - // Searching for a module by name and skipping the next line if found, - // will not always eliminate the exception caught here. - // Specifically, if the project had previously been opened and the caches were not cleared. - //noinspection ConstantConditions - FlutterModuleBuilder.addAndroidModule(project, null, moduleDir.getPath(), module.getName(), true); - } - catch (IllegalStateException ignored) { - } - // Check for a plugin example module. - File example = new File(root, "example"); - if (example.exists()) { - File android = new File(example, "android"); - File exampleFile; - if (android.exists() && (exampleFile = new File(android, moduleName + "_example_android.iml")).exists()) { - try { - //noinspection ConstantConditions - FlutterModuleBuilder.addAndroidModuleFromFile(project, null, - LocalFileSystem.getInstance().findFileByIoFile(exampleFile)); - } - catch (IllegalStateException ignored) { - } - } - } - } - }); - }); - } - }); - } - - @SuppressWarnings("ConstantConditions") - private static void scheduleDisconnectIfCancelled(MessageBusConnection[] connection) { - // If the import was cancelled the subscription will never be removed. - ApplicationManager.getApplication().executeOnPooledThread(() -> { - Project project = ProjectManager.getInstance().getDefaultProject(); - try { - Thread.sleep(300000L); // Allow five minutes to complete the project import wizard. - } - catch (InterruptedException ignored) { - } - if (connection[0] != null) { - connection[0].disconnect(); - connection[0] = null; - } - }); } @SuppressWarnings("ConstantConditions") diff --git a/src/io/flutter/run/FlutterPopFrameAction.java b/src/io/flutter/run/FlutterPopFrameAction.java index d25442b18..28762e1dc 100644 --- a/src/io/flutter/run/FlutterPopFrameAction.java +++ b/src/io/flutter/run/FlutterPopFrameAction.java @@ -36,6 +36,7 @@ public void update(@NotNull AnActionEvent e) { final DartVmServiceStackFrame frame = getStackFrame(e); final boolean enabled = frame != null && frame.canDrop(); + // noinspection deprecation if (ActionPlaces.isMainMenuOrActionSearch(e.getPlace()) || ActionPlaces.DEBUGGER_TOOLBAR.equals(e.getPlace())) { e.getPresentation().setEnabled(enabled); } diff --git a/src/io/flutter/run/FlutterPositionMapper.java b/src/io/flutter/run/FlutterPositionMapper.java index 6f3fc196f..b14232121 100644 --- a/src/io/flutter/run/FlutterPositionMapper.java +++ b/src/io/flutter/run/FlutterPositionMapper.java @@ -12,6 +12,7 @@ import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.GlobalSearchScopesCore; @@ -31,9 +32,11 @@ import org.jetbrains.annotations.Nullable; import java.io.File; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -158,7 +161,15 @@ private String findRemoteSourceRoot(String remotePath) { final PsiFile[] localFilesWithSameName = OpenApiUtils.safeRunReadAction(() -> { final String remoteFileName = PathUtil.getFileName(remotePath); final GlobalSearchScope scope = GlobalSearchScopesCore.directoryScope(project, sourceRoot, true); - return FilenameIndex.getFilesByName(project, remoteFileName, scope); + final List files = new ArrayList<>(); + FilenameIndex.processFilesByName(remoteFileName, false, scope, virtualFile -> { + PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile); + if (psiFile != null) { + files.add(psiFile); + } + return true; + }); + return files.toArray(PsiFile.EMPTY_ARRAY); }); String match = null; diff --git a/src/io/flutter/run/bazelTest/BazelTestConfigProducer.java b/src/io/flutter/run/bazelTest/BazelTestConfigProducer.java index 5df852f59..f8e83f3e2 100644 --- a/src/io/flutter/run/bazelTest/BazelTestConfigProducer.java +++ b/src/io/flutter/run/bazelTest/BazelTestConfigProducer.java @@ -32,18 +32,22 @@ public class BazelTestConfigProducer extends RunConfigurationProducer configuration) { - super(configuration); - super.setCoverageRunner(CoverageRunner.getInstance(FlutterCoverageRunner.class)); + super(configuration, CoverageRunner.getInstance(FlutterCoverageRunner.class)); createCoverageFile(); ModalityUiUtil.invokeLaterIfNeeded( ModalityState.any(), diff --git a/src/io/flutter/run/coverage/FlutterCoverageEngine.java b/src/io/flutter/run/coverage/FlutterCoverageEngine.java index d6a647e94..4b433ca29 100644 --- a/src/io/flutter/run/coverage/FlutterCoverageEngine.java +++ b/src/io/flutter/run/coverage/FlutterCoverageEngine.java @@ -49,6 +49,7 @@ public boolean canHavePerTestCoverage(@NotNull RunConfigurationBase conf) { } @Override + @SuppressWarnings("deprecation") public @Nullable CoverageSuite createCoverageSuite(@NotNull CoverageRunner covRunner, @NotNull String name, @NotNull CoverageFileProvider coverageDataFileProvider, @@ -63,6 +64,7 @@ public boolean canHavePerTestCoverage(@NotNull RunConfigurationBase conf) { } @Override + @SuppressWarnings("deprecation") public @Nullable CoverageSuite createCoverageSuite(@NotNull CoverageRunner covRunner, @NotNull String name, @NotNull CoverageFileProvider coverageDataFileProvider, diff --git a/src/io/flutter/run/coverage/FlutterCoverageProgramRunner.java b/src/io/flutter/run/coverage/FlutterCoverageProgramRunner.java index fa4150abd..c93874b89 100644 --- a/src/io/flutter/run/coverage/FlutterCoverageProgramRunner.java +++ b/src/io/flutter/run/coverage/FlutterCoverageProgramRunner.java @@ -13,7 +13,8 @@ import com.intellij.execution.configurations.RunProfileState; import com.intellij.execution.configurations.RunnerSettings; import com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration; -import com.intellij.execution.process.ProcessAdapter; + +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.runners.DefaultProgramRunnerKt; @@ -42,7 +43,7 @@ public class FlutterCoverageProgramRunner extends GenericProgramRunner processCoverage(env)); diff --git a/src/io/flutter/run/coverage/FlutterCoverageSuite.java b/src/io/flutter/run/coverage/FlutterCoverageSuite.java index 6ba6f1810..5c9a64b6e 100644 --- a/src/io/flutter/run/coverage/FlutterCoverageSuite.java +++ b/src/io/flutter/run/coverage/FlutterCoverageSuite.java @@ -20,6 +20,7 @@ public FlutterCoverageSuite(@NotNull FlutterCoverageEngine coverageEngine) { this.coverageEngine = coverageEngine; } + @SuppressWarnings("deprecation") public FlutterCoverageSuite(CoverageRunner runner, String name, CoverageFileProvider coverageDataFileProvider, diff --git a/src/io/flutter/run/daemon/DaemonApi.java b/src/io/flutter/run/daemon/DaemonApi.java index 2c82cda60..e4e21fe73 100644 --- a/src/io/flutter/run/daemon/DaemonApi.java +++ b/src/io/flutter/run/daemon/DaemonApi.java @@ -12,9 +12,10 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; -import com.intellij.execution.process.ProcessAdapter; + import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Key; @@ -120,7 +121,7 @@ CompletableFuture enableDeviceEvents() { * Receive responses and events from a process until it shuts down. */ void listen(@NotNull ProcessHandler process, @NotNull DaemonEvent.Listener listener) { - process.addProcessListener(new ProcessAdapter() { + process.addProcessListener(new ProcessListener() { @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { if (outputType.equals(ProcessOutputTypes.STDERR)) { diff --git a/src/io/flutter/run/daemon/DevToolsServerTask.java b/src/io/flutter/run/daemon/DevToolsServerTask.java index b93148449..48a5a3a8d 100644 --- a/src/io/flutter/run/daemon/DevToolsServerTask.java +++ b/src/io/flutter/run/daemon/DevToolsServerTask.java @@ -11,7 +11,9 @@ import com.google.gson.JsonSyntaxException; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; -import com.intellij.execution.process.ProcessAdapter; + +import com.intellij.execution.process.ProcessListener; +import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; import com.intellij.notification.Notification; @@ -139,7 +141,7 @@ private void setUpLocalServer(@NotNull String localDevToolsDir) throws java.util private void setUpInDevMode(@NotNull GeneralCommandLine command) { try { this.process = new MostlySilentColoredProcessHandler(command); - this.process.addProcessListener(new ProcessAdapter() { + this.process.addProcessListener(new ProcessListener() { @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { final String text = event.getText().trim(); @@ -159,7 +161,17 @@ public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType private void setUpWithDart(GeneralCommandLine command) { try { this.process = new MostlySilentColoredProcessHandler(command); - this.process.addProcessListener(new ProcessAdapter() { + this.process.addProcessListener(new ProcessListener() { + @Override + public void startNotified(@NotNull ProcessEvent event) { + } + + @Override + public void processTerminated(@NotNull ProcessEvent event) { + DevToolsServerTask.this.process.removeProcessListener(this); + DevToolsServerTask.this.process = null; + } + @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { tryParseStartupText(event.getText().trim()); diff --git a/src/io/flutter/run/daemon/DeviceService.java b/src/io/flutter/run/daemon/DeviceService.java index 72d65e90f..059ad7f6f 100644 --- a/src/io/flutter/run/daemon/DeviceService.java +++ b/src/io/flutter/run/daemon/DeviceService.java @@ -185,10 +185,12 @@ private void fireChangeEvent() { *

*

This might mean starting it, stopping it, or restarting it. */ + @SuppressWarnings("unchecked") private void refreshDeviceDaemon() { ApplicationManager.getApplication().executeOnPooledThread(() -> { DumbService.getInstance(project).waitForSmartMode(); if (project.isDisposed()) return; + // noinspection unchecked deviceDaemon.refresh(this::chooseNextDaemon); refreshInProgress = false; ActivityTracker.getInstance().inc(); @@ -273,7 +275,9 @@ public void restart() { JobScheduler.getScheduler().schedule(this::refreshDeviceDaemon, 4, TimeUnit.SECONDS); } + @SuppressWarnings("unchecked") private void shutDown() { + // noinspection unchecked deviceDaemon.refresh(this::shutDownDaemon); } diff --git a/src/io/flutter/run/daemon/FlutterApp.java b/src/io/flutter/run/daemon/FlutterApp.java index c4532d1bf..bff80b59d 100644 --- a/src/io/flutter/run/daemon/FlutterApp.java +++ b/src/io/flutter/run/daemon/FlutterApp.java @@ -10,9 +10,10 @@ import com.google.gson.JsonPrimitive; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; -import com.intellij.execution.process.ProcessAdapter; + import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.ui.ConsoleView; import com.intellij.execution.ui.ConsoleViewContentType; @@ -257,7 +258,7 @@ public static FlutterApp start(@NotNull ExecutionEnvironment env, final DaemonApi api = new DaemonApi(process); final FlutterApp app = new FlutterApp(project, mode, device, process, env, api, command); - process.addProcessListener(new ProcessAdapter() { + process.addProcessListener(new ProcessListener() { @Override public void processTerminated(@NotNull ProcessEvent event) { LOG.info(analyticsStop + " " + project.getName() + " (" + mode.mode() + ")"); diff --git a/src/io/flutter/run/test/FlutterTestConfigProducer.java b/src/io/flutter/run/test/FlutterTestConfigProducer.java index 7660908b6..1b35f01ff 100644 --- a/src/io/flutter/run/test/FlutterTestConfigProducer.java +++ b/src/io/flutter/run/test/FlutterTestConfigProducer.java @@ -25,8 +25,9 @@ public class FlutterTestConfigProducer extends RunConfigurationProducer { private final TestConfigUtils testConfigUtils = TestConfigUtils.getInstance(); + @SuppressWarnings("deprecation") protected FlutterTestConfigProducer() { - super(FlutterTestConfigType.getInstance()); + super(FlutterTestConfigType.getInstance().getConfigurationFactories()[0]); } private static boolean isFlutterContext(@NotNull ConfigurationContext context) { diff --git a/src/io/flutter/run/test/FlutterTestRunner.java b/src/io/flutter/run/test/FlutterTestRunner.java index 4c6c9e615..c9b0885d6 100644 --- a/src/io/flutter/run/test/FlutterTestRunner.java +++ b/src/io/flutter/run/test/FlutterTestRunner.java @@ -226,7 +226,15 @@ private static final class Connector implements ObservatoryConnector { private String observatoryUri; public Connector(ProcessHandler handler) { - listener = new ProcessAdapter() { + listener = new ProcessListener() { + @Override + public void startNotified(@NotNull ProcessEvent event) { + } + + @Override + public void processTerminated(@NotNull ProcessEvent event) { + } + @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { if (!outputType.equals(ProcessOutputTypes.STDOUT)) { diff --git a/src/io/flutter/run/test/TestForm.java b/src/io/flutter/run/test/TestForm.java index 00fa4f99a..7311e4353 100644 --- a/src/io/flutter/run/test/TestForm.java +++ b/src/io/flutter/run/test/TestForm.java @@ -63,8 +63,9 @@ public void customize(final JList list, }); initDartFileTextWithBrowse(project, testFile); - testDir.addBrowseFolderListener(project, FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle("Test Directory")); + testDir.addBrowseFolderListener("Test Directory", null, project, + FileChooserDescriptorFactory.createSingleFolderDescriptor(), + com.intellij.openapi.ui.TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT); } @NotNull diff --git a/src/io/flutter/sdk/FlutterCommand.java b/src/io/flutter/sdk/FlutterCommand.java index 7310139e9..e4fa62591 100644 --- a/src/io/flutter/sdk/FlutterCommand.java +++ b/src/io/flutter/sdk/FlutterCommand.java @@ -8,7 +8,11 @@ import com.google.common.collect.ImmutableList; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; -import com.intellij.execution.process.*; +import com.intellij.execution.process.CapturingProcessAdapter; +import com.intellij.execution.process.ColoredProcessHandler; +import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.process.ProcessListener; +import com.intellij.execution.process.ProcessOutput; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; @@ -187,7 +191,7 @@ public Process startInModuleConsole(@NotNull Module module, @Nullable Runnable o if (processListener != null) { handler.addProcessListener(processListener); } - handler.addProcessListener(new ProcessAdapter() { + handler.addProcessListener(new ProcessListener() { @Override public void processTerminated(@NotNull ProcessEvent event) { if (onDone != null) { @@ -244,7 +248,7 @@ public FlutterCommandStartResult startProcess(@Nullable Project project) { final GeneralCommandLine commandLine = createGeneralCommandLine(project); LOG.info(safeCommandLog(commandLine)); handler = new MostlySilentColoredProcessHandler(commandLine); - handler.addProcessListener(new ProcessAdapter() { + handler.addProcessListener(new ProcessListener() { @Override public void processTerminated(@NotNull final ProcessEvent event) { if (isPubRelatedCommand()) { diff --git a/src/io/flutter/sdk/FlutterSdkUtil.java b/src/io/flutter/sdk/FlutterSdkUtil.java index e7b31fd5f..768fef9cb 100644 --- a/src/io/flutter/sdk/FlutterSdkUtil.java +++ b/src/io/flutter/sdk/FlutterSdkUtil.java @@ -95,7 +95,7 @@ private static void updateKnownPaths(@SuppressWarnings("SameParameterValue") @No /** * Adds the current path and other known paths to the combo, most recently used first. */ - public static void addKnownSDKPathsToCombo(@NotNull JComboBox combo) { + public static void addKnownSDKPathsToCombo(@NotNull @SuppressWarnings("rawtypes") JComboBox combo) { // First, get the current path from the combo box on the EDT. final String currentPath = combo.getEditor().getItem().toString().trim(); final Set pathsToShow = new LinkedHashSet<>(); diff --git a/src/io/flutter/utils/FlutterModuleUtils.java b/src/io/flutter/utils/FlutterModuleUtils.java index 3fc281e01..81cbed0f6 100644 --- a/src/io/flutter/utils/FlutterModuleUtils.java +++ b/src/io/flutter/utils/FlutterModuleUtils.java @@ -370,7 +370,9 @@ public static boolean isInFlutterAndroidModule(@NotNull Project project, @NotNul * Set the passed module to the module type used by Flutter, defined by {@link #getModuleTypeIDForFlutter()}. */ public static void setFlutterModuleType(@NotNull Module module) { - module.setModuleType(getModuleTypeIDForFlutter()); + if (!getModuleTypeIDForFlutter().equals(ModuleType.get(module).getId())) { + module.setModuleType(getModuleTypeIDForFlutter()); + } } public static void setFlutterModuleAndReload(@NotNull Module module, @NotNull Project project) { diff --git a/verify-ignore-problems.txt b/verify-ignore-problems.txt index 7f133435e..b68d7c55e 100644 --- a/verify-ignore-problems.txt +++ b/verify-ignore-problems.txt @@ -4,3 +4,24 @@ # See more context in https://github.com/flutter/flutter-intellij/pull/8590 ::Package 'de.roderick' is not found.* +// Internal API usages +::Internal method com.intellij.openapi.module.Module.setModuleType.* +::Internal .* com.intellij.openapi.project.impl.ProjectImpl.* +::Internal class com.intellij.openapi.project.impl.ProjectManagerImpl.* +::Internal method com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration.coverageRunnerExtensionRemoved.* +::Internal method com.intellij.coverage.SimpleCoverageAnnotator.getRoots.* +::Internal method com.intellij.coverage.CoverageEngine.recompileProjectAndRerunAction.* +::Internal method com.intellij.coverage.CoverageEngine.canHavePerTestCoverage.* +::Internal method com.intellij.coverage.CoverageEngine.getQualifiedName.* +::Internal method com.intellij.ide.impl.NewProjectUtilKt.createNewProjectAsync.* +::Internal method com.intellij.xdebugger.impl.XDebugSessionImpl.reset.* +::Internal class com.intellij.serviceContainer.ComponentManagerImpl.* +::Internal method com.intellij.openapi.startup.StartupManager.runAfterOpened.* +::Internal constructor com.intellij.ui.BadgeIcon..* +::Internal class com.intellij.ui.BadgeIcon.* +::Invocation of unresolved constructor com.intellij.openapi.project.impl.ProjectImpl..* +::module com.intellij.modules.androidstudio \(optional\): Dependency 'module com.intellij.modules.androidstudio \(optional\)' is not resolved.* +::Internal method com.intellij.util.lang.UrlClassLoader.addFiles.* +::Internal method com.intellij.execution.testframework.sm.runner.states.TestStateInfo.Magnitude.values.* +::Internal field com.intellij.execution.testframework.sm.runner.states.TestStateInfo.Magnitude.* +::Internal field com.intellij.ide.ui.UISettingsListener.TOPIC