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