new subtask action
[idea/community.git] / python / educational-core / student / src / com / jetbrains / edu / learning / StudyProjectComponent.java
1 package com.jetbrains.edu.learning;
2
3 import com.intellij.ide.ui.UISettings;
4 import com.intellij.notification.Notification;
5 import com.intellij.notification.NotificationListener;
6 import com.intellij.notification.NotificationType;
7 import com.intellij.openapi.actionSystem.*;
8 import com.intellij.openapi.actionSystem.ex.AnActionListener;
9 import com.intellij.openapi.application.ApplicationManager;
10 import com.intellij.openapi.components.ProjectComponent;
11 import com.intellij.openapi.diagnostic.Logger;
12 import com.intellij.openapi.editor.EditorFactory;
13 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
14 import com.intellij.openapi.keymap.Keymap;
15 import com.intellij.openapi.keymap.ex.KeymapManagerEx;
16 import com.intellij.openapi.module.Module;
17 import com.intellij.openapi.module.ModuleManager;
18 import com.intellij.openapi.progress.ProgressManager;
19 import com.intellij.openapi.project.DumbAwareRunnable;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.startup.StartupManager;
22 import com.intellij.openapi.util.Pair;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.vfs.VirtualFile;
25 import com.intellij.openapi.vfs.VirtualFileAdapter;
26 import com.intellij.openapi.vfs.VirtualFileEvent;
27 import com.intellij.openapi.vfs.VirtualFileManager;
28 import com.intellij.openapi.wm.ToolWindow;
29 import com.intellij.openapi.wm.ToolWindowManager;
30 import com.intellij.util.containers.hash.HashMap;
31 import com.jetbrains.edu.learning.actions.StudyActionWithShortcut;
32 import com.jetbrains.edu.learning.actions.StudyNextWindowAction;
33 import com.jetbrains.edu.learning.actions.StudyPrevWindowAction;
34 import com.jetbrains.edu.learning.core.EduNames;
35 import com.jetbrains.edu.learning.core.EduUtils;
36 import com.jetbrains.edu.learning.courseFormat.*;
37 import com.jetbrains.edu.learning.editor.StudyEditorFactoryListener;
38 import com.jetbrains.edu.learning.statistics.EduUsagesCollector;
39 import com.jetbrains.edu.learning.stepic.CourseInfo;
40 import com.jetbrains.edu.learning.stepic.EduStepicConnector;
41 import com.jetbrains.edu.learning.ui.StudyToolWindow;
42 import com.jetbrains.edu.learning.ui.StudyToolWindowFactory;
43 import javafx.application.Platform;
44 import org.jetbrains.annotations.NotNull;
45
46 import javax.swing.*;
47 import javax.swing.event.HyperlinkEvent;
48 import java.io.File;
49 import java.io.IOException;
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.Map;
53
54 import static com.jetbrains.edu.learning.StudyUtils.execCancelable;
55 import static com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator.flushCourse;
56
57
58 public class StudyProjectComponent implements ProjectComponent {
59   private static final Logger LOG = Logger.getInstance(StudyProjectComponent.class.getName());
60   private final Project myProject;
61   private FileCreatedByUserListener myListener;
62   private Map<Keymap, List<Pair<String, String>>> myDeletedShortcuts = new HashMap<>();
63   private StudyProjectComponent(@NotNull final Project project) {
64     myProject = project;
65   }
66
67   @Override
68   public void projectOpened() {
69     Course course = StudyTaskManager.getInstance(myProject).getCourse();
70     // Check if user has javafx lib in his JDK. Now bundled JDK doesn't have this lib inside.
71     if (StudyUtils.hasJavaFx()) {
72       Platform.setImplicitExit(false);
73     }
74
75     if (course != null && !course.isAdaptive() && !course.isUpToDate()) {
76       final Notification notification =
77         new Notification("Update.course", "Course Updates", "Course is ready to <a href=\"update\">update</a>", NotificationType.INFORMATION,
78                          new NotificationListener() {
79                            @Override
80                            public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
81                              FileEditorManagerEx.getInstanceEx(myProject).closeAllFiles();
82
83                              ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
84                                ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true);
85                                return execCancelable(() -> {
86                                  updateCourse();
87                                  return true;
88                                });
89                              }, "Updating Course", true, myProject);
90                              EduUtils.synchronize();
91                              course.setUpdated();
92
93                            }
94                          });
95       notification.notify(myProject);
96
97     }
98
99     StudyUtils.registerStudyToolWindow(course, myProject);
100     StartupManager.getInstance(myProject).runWhenProjectIsInitialized(() -> ApplicationManager.getApplication().invokeLater(
101       (DumbAwareRunnable)() -> ApplicationManager.getApplication().runWriteAction((DumbAwareRunnable)() -> {
102         Course course1 = StudyTaskManager.getInstance(myProject).getCourse();
103         if (course1 != null) {
104           final UISettings instance = UISettings.getInstance();
105           if (instance != null) {
106             instance.HIDE_TOOL_STRIPES = false;
107             instance.fireUISettingsChanged();
108           }
109           registerShortcuts();
110           EduUsagesCollector.projectTypeOpened(course1.isAdaptive() ? EduNames.ADAPTIVE : EduNames.STUDY);
111         }
112       })));
113   }
114
115   private void registerShortcuts() {
116     StudyToolWindow window = StudyUtils.getStudyToolWindow(myProject);
117     if (window != null) {
118       List<AnAction> actionsOnToolbar = window.getActions();
119       if (actionsOnToolbar != null) {
120         for (AnAction action : actionsOnToolbar) {
121           if (action instanceof StudyActionWithShortcut) {
122             String id = ((StudyActionWithShortcut)action).getActionId();
123             String[] shortcuts = ((StudyActionWithShortcut)action).getShortcuts();
124             if (shortcuts != null) {
125               addShortcut(id, shortcuts);
126             }
127           }
128         }
129         addShortcut(StudyNextWindowAction.ACTION_ID, new String[]{StudyNextWindowAction.SHORTCUT, StudyNextWindowAction.SHORTCUT2});
130         addShortcut(StudyPrevWindowAction.ACTION_ID, new String[]{StudyPrevWindowAction.SHORTCUT});
131       }
132       else {
133         LOG.warn("Actions on toolbar are nulls");
134       }
135     }
136   }
137
138   private void updateCourse() {
139     final Course currentCourse = StudyTaskManager.getInstance(myProject).getCourse();
140     final CourseInfo info = CourseInfo.fromCourse(currentCourse);
141     if (info == null) return;
142
143     final File resourceDirectory = new File(currentCourse.getCourseDirectory());
144     if (resourceDirectory.exists()) {
145       FileUtil.delete(resourceDirectory);
146     }
147
148     final Course course = EduStepicConnector.getCourse(myProject, info);
149
150     if (course == null) return;
151     flushCourse(myProject, course);
152     course.initCourse(false);
153
154     StudyLanguageManager manager = StudyUtils.getLanguageManager(course);
155     if (manager == null) {
156       LOG.info("Study Language Manager is null for " + course.getLanguageById().getDisplayName());
157       return;
158     }
159
160     final ArrayList<Lesson> updatedLessons = new ArrayList<>();
161
162     int lessonIndex = 0;
163     for (Lesson lesson : course.getLessons()) {
164       lessonIndex += 1;
165       Lesson studentLesson = currentCourse.getLesson(lesson.getId());
166       final String lessonDirName = EduNames.LESSON + String.valueOf(lessonIndex);
167
168       final File lessonDir = new File(myProject.getBasePath(), lessonDirName);
169       if (!lessonDir.exists()){
170         final File fromLesson = new File(resourceDirectory, lessonDirName);
171         try {
172           FileUtil.copyDir(fromLesson, lessonDir);
173         }
174         catch (IOException e) {
175           LOG.warn("Failed to copy lesson " + fromLesson.getPath());
176         }
177         lesson.setIndex(lessonIndex);
178         lesson.initLesson(currentCourse, false);
179         for (int i = 1; i <= lesson.getTaskList().size(); i++) {
180           Task task = lesson.getTaskList().get(i - 1);
181           task.setIndex(i);
182         }
183         updatedLessons.add(lesson);
184         continue;
185       }
186       studentLesson.setIndex(lessonIndex);
187       updatedLessons.add(studentLesson);
188
189       int index = 0;
190       final ArrayList<Task> tasks = new ArrayList<>();
191       for (Task task : lesson.getTaskList()) {
192         index += 1;
193         final Task studentTask = studentLesson.getTask(task.getStepId());
194         if (studentTask != null && StudyStatus.Solved.equals(studentTask.getStatus())) {
195           studentTask.setIndex(index);
196           tasks.add(studentTask);
197           continue;
198         }
199         task.initTask(studentLesson, false);
200         task.setIndex(index);
201
202         final String taskDirName = EduNames.TASK + String.valueOf(index);
203         final File toTask = new File(lessonDir, taskDirName);
204
205         final String taskPath = FileUtil.join(resourceDirectory.getPath(), lessonDirName, taskDirName);
206         final File taskDir = new File(taskPath);
207         if (!taskDir.exists()) return;
208         final File[] taskFiles = taskDir.listFiles();
209         if (taskFiles == null) continue;
210         for (File fromFile : taskFiles) {
211           copyFile(fromFile, new File(toTask, fromFile.getName()));
212         }
213         tasks.add(task);
214       }
215       studentLesson.updateTaskList(tasks);
216     }
217     currentCourse.setLessons(updatedLessons);
218
219     final Notification notification =
220       new Notification("Update.course", "Course update", "Current course is synchronized", NotificationType.INFORMATION);
221     notification.notify(myProject);
222   }
223
224   private static void copyFile(@NotNull final File from, @NotNull final File to) {
225     if (from.exists()) {
226       try {
227         FileUtil.copy(from, to);
228       }
229       catch (IOException e) {
230         LOG.warn("Failed to copy " + from.getName());
231       }
232     }
233   }
234
235   private void addShortcut(@NotNull final String actionIdString, @NotNull final String[] shortcuts) {
236     KeymapManagerEx keymapManager = KeymapManagerEx.getInstanceEx();
237     for (Keymap keymap : keymapManager.getAllKeymaps()) {
238       List<Pair<String, String>> pairs = myDeletedShortcuts.get(keymap);
239       if (pairs == null) {
240         pairs = new ArrayList<>();
241         myDeletedShortcuts.put(keymap, pairs);
242       }
243       for (String shortcutString : shortcuts) {
244         Shortcut studyActionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcutString), null);
245         String[] actionsIds = keymap.getActionIds(studyActionShortcut);
246         for (String actionId : actionsIds) {
247           pairs.add(Pair.create(actionId, shortcutString));
248           keymap.removeShortcut(actionId, studyActionShortcut);
249         }
250         keymap.addShortcut(actionIdString, studyActionShortcut);
251       }
252     }
253   }
254
255   @Override
256   public void projectClosed() {
257     final Course course = StudyTaskManager.getInstance(myProject).getCourse();
258     if (course != null) {
259       final ToolWindow toolWindow = ToolWindowManager.getInstance(myProject).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW);
260       if (toolWindow != null) {
261         toolWindow.getContentManager().removeAllContents(false);
262       }
263       KeymapManagerEx keymapManager = KeymapManagerEx.getInstanceEx();
264       for (Keymap keymap : keymapManager.getAllKeymaps()) {
265         List<Pair<String, String>> pairs = myDeletedShortcuts.get(keymap);
266         if (pairs != null && !pairs.isEmpty()) {
267           for (Pair<String, String> actionShortcut : pairs) {
268             keymap.addShortcut(actionShortcut.first, new KeyboardShortcut(KeyStroke.getKeyStroke(actionShortcut.second), null));
269           }
270         }
271       }
272     }
273     myListener = null;
274   }
275
276   @Override
277   public void initComponent() {
278     EditorFactory.getInstance().addEditorFactoryListener(new StudyEditorFactoryListener(), myProject);
279     ActionManager.getInstance().addAnActionListener(new AnActionListener() {
280       @Override
281       public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
282         AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
283         for (AnAction newAction : newGroupActions) {
284           if (newAction == action) {
285             myListener =  new FileCreatedByUserListener();
286             VirtualFileManager.getInstance().addVirtualFileListener(myListener);
287             break;
288           }
289         }
290       }
291
292       @Override
293       public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
294         AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
295         for (AnAction newAction : newGroupActions) {
296           if (newAction == action) {
297             VirtualFileManager.getInstance().removeVirtualFileListener(myListener);
298           }
299         }
300       }
301
302       @Override
303       public void beforeEditorTyping(char c, DataContext dataContext) {
304
305       }
306     });
307   }
308
309   @Override
310   public void disposeComponent() {
311   }
312
313   @NotNull
314   @Override
315   public String getComponentName() {
316     return "StudyTaskManager";
317   }
318
319   public static StudyProjectComponent getInstance(@NotNull final Project project) {
320     final Module module = ModuleManager.getInstance(project).getModules()[0];
321     return module.getComponent(StudyProjectComponent.class);
322   }
323
324   private class FileCreatedByUserListener extends VirtualFileAdapter {
325     @Override
326     public void fileCreated(@NotNull VirtualFileEvent event) {
327       if (myProject.isDisposed()) return;
328       final VirtualFile createdFile = event.getFile();
329       final VirtualFile taskDir = createdFile.getParent();
330       final Course course = StudyTaskManager.getInstance(myProject).getCourse();
331       if (course == null || !EduNames.STUDY.equals(course.getCourseMode())) {
332         return;
333       }
334       if (taskDir != null && taskDir.getName().contains(EduNames.TASK)) {
335         int taskIndex = EduUtils.getIndex(taskDir.getName(), EduNames.TASK);
336         final VirtualFile lessonDir = taskDir.getParent();
337         if (lessonDir != null && lessonDir.getName().contains(EduNames.LESSON)) {
338           int lessonIndex = EduUtils.getIndex(lessonDir.getName(), EduNames.LESSON);
339           List<Lesson> lessons = course.getLessons();
340           if (StudyUtils.indexIsValid(lessonIndex, lessons)) {
341             final Lesson lesson = lessons.get(lessonIndex);
342             final List<Task> tasks = lesson.getTaskList();
343             if (StudyUtils.indexIsValid(taskIndex, tasks)) {
344               final Task task = tasks.get(taskIndex);
345               final TaskFile taskFile = new TaskFile();
346               taskFile.initTaskFile(task, false);
347               taskFile.setUserCreated(true);
348               final String name = createdFile.getName();
349               taskFile.name = name;
350               //TODO: put to other steps as well
351               task.getTaskFiles().put(name, taskFile);
352             }
353           }
354         }
355       }
356     }
357   }
358 }