synchronize courses after update for custom projects
[idea/community.git] / python / edu / learn-python / src / com / jetbrains / python / edu / StudyTaskManager.java
1 package com.jetbrains.python.edu;
2
3 import com.intellij.ide.BrowserUtil;
4 import com.intellij.ide.ui.UISettings;
5 import com.intellij.ide.util.PropertiesComponent;
6 import com.intellij.notification.Notification;
7 import com.intellij.notification.NotificationListener;
8 import com.intellij.notification.NotificationType;
9 import com.intellij.notification.Notifications;
10 import com.intellij.openapi.actionSystem.*;
11 import com.intellij.openapi.actionSystem.ex.AnActionListener;
12 import com.intellij.openapi.application.ApplicationManager;
13 import com.intellij.openapi.components.*;
14 import com.intellij.openapi.diagnostic.Logger;
15 import com.intellij.openapi.editor.EditorFactory;
16 import com.intellij.openapi.fileEditor.FileEditor;
17 import com.intellij.openapi.fileEditor.FileEditorManager;
18 import com.intellij.openapi.keymap.Keymap;
19 import com.intellij.openapi.keymap.KeymapManager;
20 import com.intellij.openapi.project.DumbAware;
21 import com.intellij.openapi.project.DumbAwareRunnable;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.startup.StartupManager;
24 import com.intellij.openapi.ui.popup.Balloon;
25 import com.intellij.openapi.ui.popup.JBPopupAdapter;
26 import com.intellij.openapi.ui.popup.LightweightWindowEvent;
27 import com.intellij.openapi.util.io.FileUtil;
28 import com.intellij.openapi.vfs.VirtualFile;
29 import com.intellij.openapi.vfs.VirtualFileAdapter;
30 import com.intellij.openapi.vfs.VirtualFileEvent;
31 import com.intellij.openapi.vfs.VirtualFileManager;
32 import com.intellij.openapi.wm.*;
33 import com.intellij.util.ui.update.UiNotifyConnector;
34 import com.intellij.util.xmlb.XmlSerializer;
35 import com.jetbrains.python.edu.actions.*;
36 import com.jetbrains.python.edu.course.Course;
37 import com.jetbrains.python.edu.course.Lesson;
38 import com.jetbrains.python.edu.course.Task;
39 import com.jetbrains.python.edu.course.TaskFile;
40 import com.jetbrains.python.edu.ui.StudyCondition;
41 import com.jetbrains.python.edu.ui.StudyToolWindowFactory;
42 import org.jdom.Element;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45
46 import javax.swing.*;
47 import javax.swing.event.HyperlinkEvent;
48 import java.io.File;
49 import java.io.IOException;
50 import java.lang.reflect.Method;
51 import java.net.URL;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55
56 /**
57  * Implementation of class which contains all the information
58  * about study in context of current project
59  */
60
61 @State(
62   name = "StudySettings",
63   storages = {
64     @Storage(
65       id = "others",
66       file = "$PROJECT_CONFIG_DIR$/study_project.xml",
67       scheme = StorageScheme.DIRECTORY_BASED
68     )}
69 )
70 public class StudyTaskManager implements ProjectComponent, PersistentStateComponent<Element>, DumbAware {
71   private static final Logger LOG = Logger.getInstance(StudyTaskManager.class.getName());
72   public static final String COURSE_ELEMENT = "courseElement";
73   private static Map<String, StudyTaskManager> myTaskManagers = new HashMap<String, StudyTaskManager>();
74   private static Map<String, String> myDeletedShortcuts = new HashMap<String, String>();
75   private final Project myProject;
76   private Course myCourse;
77   private FileCreatedListener myListener;
78
79
80   public void setCourse(Course course) {
81     myCourse = course;
82   }
83
84   private StudyTaskManager(@NotNull final Project project) {
85     myTaskManagers.put(project.getBasePath(), this);
86     myProject = project;
87   }
88
89
90   @Nullable
91   public Course getCourse() {
92     return myCourse;
93   }
94
95   @Nullable
96   @Override
97   public Element getState() {
98     Element el = new Element("taskManager");
99     if (myCourse != null) {
100       Element courseElement = new Element(COURSE_ELEMENT);
101       XmlSerializer.serializeInto(myCourse, courseElement);
102       el.addContent(courseElement);
103     }
104     return el;
105   }
106
107   @Override
108   public void loadState(Element el) {
109     myCourse = XmlSerializer.deserialize(el.getChild(COURSE_ELEMENT), Course.class);
110     if (myCourse != null) {
111       myCourse.init(true);
112     }
113   }
114
115   @Override
116   public void projectOpened() {
117     if (myCourse != null && !myCourse.isUpToDate()) {
118       myCourse.setUpToDate(true);
119       updateCourse();
120     }
121     ApplicationManager.getApplication().invokeLater(new DumbAwareRunnable() {
122       @Override
123       public void run() {
124         ApplicationManager.getApplication().runWriteAction(new DumbAwareRunnable() {
125           @Override
126           public void run() {
127             if (myCourse != null) {
128               StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() {
129                 @Override
130                 public void run() {
131                   ToolWindowManager.getInstance(myProject).getToolWindow(ToolWindowId.PROJECT_VIEW).show(new Runnable() {
132                     @Override
133                     public void run() {
134                       FileEditor[] editors = FileEditorManager.getInstance(myProject).getSelectedEditors();
135                       if (editors.length > 0) {
136                         final JComponent focusedComponent = editors[0].getPreferredFocusedComponent();
137                         if (focusedComponent != null) {
138                           ApplicationManager.getApplication().invokeLater(new Runnable() {
139                             @Override
140                             public void run() {
141                               IdeFocusManager.getInstance(myProject).requestFocus(focusedComponent, true);
142                             }
143                           });
144                         }
145                       }
146                     }
147                   });
148                 }
149               });
150               UISettings.getInstance().HIDE_TOOL_STRIPES = false;
151               UISettings.getInstance().fireUISettingsChanged();
152               ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(myProject);
153               String toolWindowId = StudyToolWindowFactory.STUDY_TOOL_WINDOW;
154               try {
155                 Method method = toolWindowManager.getClass().getDeclaredMethod("registerToolWindow", String.class,
156                                                                                JComponent.class,
157                                                                                ToolWindowAnchor.class,
158                                                                                boolean.class, boolean.class, boolean.class);
159                 method.setAccessible(true);
160                 method.invoke(toolWindowManager, toolWindowId, null, ToolWindowAnchor.LEFT, true, true, true);
161               }
162               catch (Exception e) {
163                 final ToolWindow toolWindow = toolWindowManager.getToolWindow(toolWindowId);
164                 if (toolWindow == null) {
165                   toolWindowManager.registerToolWindow(toolWindowId, true, ToolWindowAnchor.RIGHT, myProject, true);
166                 }
167               }
168
169               final ToolWindow studyToolWindow = toolWindowManager.getToolWindow(toolWindowId);
170               class UrlOpeningListener implements NotificationListener {
171                 private final boolean myExpireNotification;
172
173                 public UrlOpeningListener(boolean expireNotification) {
174                   myExpireNotification = expireNotification;
175                 }
176
177                 protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
178                   URL url = event.getURL();
179                   if (url == null) {
180                     BrowserUtil.browse(event.getDescription());
181                   }
182                   else {
183                     BrowserUtil.browse(url);
184                   }
185                   if (myExpireNotification) {
186                     notification.expire();
187                   }
188                 }
189
190                 @Override
191                 public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
192                   if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
193                     hyperlinkActivated(notification, event);
194                   }
195                 }
196               }
197               if (studyToolWindow != null) {
198                 StudyUtils.updateStudyToolWindow(myProject);
199                 studyToolWindow.show(null);
200                 UiNotifyConnector.doWhenFirstShown(studyToolWindow.getComponent(), new Runnable() {
201                   @Override
202                   public void run() {
203                     if (PropertiesComponent.getInstance().getBoolean("StudyShowPopup", true)) {
204                       String content = "<html>If you'd like to learn" +
205                                        " more about PyCharm " +
206                                        "Educational Edition, " +
207                                        "click <a href=\"https://www.jetbrains.com/pycharm-educational/quickstart/\">here</a> to watch a tutorial</html>";
208                       final Notification notification =
209                         new Notification("Watch Tutorials!", "", content, NotificationType.INFORMATION, new UrlOpeningListener(true));
210                       Notifications.Bus.notify(notification);
211                       Balloon balloon = notification.getBalloon();
212                       if (balloon != null) {
213                         balloon.addListener(new JBPopupAdapter() {
214                           @Override
215                           public void onClosed(LightweightWindowEvent event) {
216                             notification.expire();
217                           }
218                         });
219                       }
220                       notification.whenExpired(new Runnable() {
221                         @Override
222                         public void run() {
223                           PropertiesComponent.getInstance().setValue("StudyShowPopup", String.valueOf(false));
224                         }
225                       });
226                     }
227                   }
228                 });
229               }
230               addShortcut(StudyNextWindowAction.SHORTCUT, StudyNextWindowAction.ACTION_ID, false);
231               addShortcut(StudyPrevWindowAction.SHORTCUT, StudyPrevWindowAction.ACTION_ID, false);
232               addShortcut(StudyShowHintAction.SHORTCUT, StudyShowHintAction.ACTION_ID, false);
233               addShortcut(StudyNextWindowAction.SHORTCUT2, StudyNextWindowAction.ACTION_ID, true);
234               addShortcut(StudyCheckAction.SHORTCUT, StudyCheckAction.ACTION_ID, false);
235               addShortcut(StudyNextStudyTaskAction.SHORTCUT, StudyNextStudyTaskAction.ACTION_ID, false);
236               addShortcut(StudyPreviousStudyTaskAction.SHORTCUT, StudyPreviousStudyTaskAction.ACTION_ID, false);
237               addShortcut(StudyRefreshTaskFileAction.SHORTCUT, StudyRefreshTaskFileAction.ACTION_ID, false);
238             }
239           }
240         });
241       }
242     });
243   }
244
245   private void updateCourse() {
246     if (myCourse == null) {
247       return;
248     }
249     File resourceFile = new File(myCourse.getResourcePath());
250     if (!resourceFile.exists()) {
251       return;
252     }
253     final File courseDir = resourceFile.getParentFile();
254     if (!courseDir.exists()) {
255       return;
256     }
257     final File[] files = courseDir.listFiles();
258     if (files == null) return;
259     for (File lesson : files) {
260       if (lesson.getName().startsWith(StudyNames.LESSON)) {
261         final File[] tasks = lesson.listFiles();
262         if (tasks == null) continue;
263         for (File task : tasks) {
264           final File taskDescr = new File(task, StudyNames.TASK_HTML);
265           final File taskTests = new File(task, StudyNames.TASK_TESTS);
266           copyFile(lesson, task, taskDescr, StudyNames.TASK_HTML);
267           copyFile(lesson, task, taskTests, StudyNames.TASK_TESTS);
268         }
269       }
270     }
271
272     final Notification notification =
273       new Notification("Update.course", "Course update", "Current course is synchronized", NotificationType.INFORMATION);
274     notification.notify(myProject);
275   }
276
277   private void copyFile(@NotNull final File lesson, @NotNull final File task, @NotNull final File taskDescr,
278                         @NotNull final String fileName) {
279     if (taskDescr.exists()) {
280       try {
281         FileUtil.copy(taskDescr, new File(new File(new File(myProject.getBasePath(), lesson.getName()), task.getName()), fileName));
282       }
283       catch (IOException e) {
284         LOG.warn("Failed to copy " + lesson.getName() + " " + task.getName());
285       }
286     }
287   }
288
289   private static void addShortcut(@NotNull final String shortcutString, @NotNull final String actionIdString, boolean isAdditional) {
290     Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
291     Shortcut[] shortcuts = keymap.getShortcuts(actionIdString);
292     if (shortcuts.length > 0 && !isAdditional) {
293       return;
294     }
295     Shortcut studyActionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcutString), null);
296     String[] actionsIds = keymap.getActionIds(studyActionShortcut);
297     for (String actionId : actionsIds) {
298       myDeletedShortcuts.put(actionId, shortcutString);
299       keymap.removeShortcut(actionId, studyActionShortcut);
300     }
301     keymap.addShortcut(actionIdString, studyActionShortcut);
302   }
303
304   @Override
305   public void projectClosed() {
306     StudyCondition.VALUE = false;
307     if (myCourse != null) {
308       ToolWindowManager.getInstance(myProject).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW).getContentManager()
309         .removeAllContents(false);
310       if (!myDeletedShortcuts.isEmpty()) {
311         for (Map.Entry<String, String> shortcut : myDeletedShortcuts.entrySet()) {
312           Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
313           Shortcut actionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcut.getValue()), null);
314           keymap.addShortcut(shortcut.getKey(), actionShortcut);
315         }
316       }
317     }
318   }
319
320   @Override
321   public void initComponent() {
322     EditorFactory.getInstance().addEditorFactoryListener(new StudyEditorFactoryListener(), myProject);
323     ActionManager.getInstance().addAnActionListener(new AnActionListener() {
324       @Override
325       public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
326         AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
327         for (AnAction newAction : newGroupActions) {
328           if (newAction == action) {
329             myListener =  new FileCreatedListener();
330             VirtualFileManager.getInstance().addVirtualFileListener(myListener);
331             break;
332           }
333         }
334       }
335
336       @Override
337       public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
338         AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
339         for (AnAction newAction : newGroupActions) {
340           if (newAction == action) {
341             VirtualFileManager.getInstance().removeVirtualFileListener(myListener);
342           }
343         }
344       }
345
346       @Override
347       public void beforeEditorTyping(char c, DataContext dataContext) {
348
349       }
350     });
351   }
352
353   @Override
354   public void disposeComponent() {
355   }
356
357   @NotNull
358   @Override
359   public String getComponentName() {
360     return "StudyTaskManager";
361   }
362
363   public static StudyTaskManager getInstance(@NotNull final Project project) {
364     StudyTaskManager item = myTaskManagers.get(project.getBasePath());
365     return item != null ? item : new StudyTaskManager(project);
366   }
367
368
369   @Nullable
370   public TaskFile getTaskFile(@NotNull final VirtualFile file) {
371     if (myCourse == null) {
372       return null;
373     }
374     VirtualFile taskDir = file.getParent();
375     if (taskDir != null) {
376       String taskDirName = taskDir.getName();
377       if (taskDirName.contains(Task.TASK_DIR)) {
378         VirtualFile lessonDir = taskDir.getParent();
379         if (lessonDir != null) {
380           String lessonDirName = lessonDir.getName();
381           int lessonIndex = StudyUtils.getIndex(lessonDirName, Lesson.LESSON_DIR);
382           List<Lesson> lessons = myCourse.getLessons();
383           if (!StudyUtils.indexIsValid(lessonIndex, lessons)) {
384             return null;
385           }
386           Lesson lesson = lessons.get(lessonIndex);
387           int taskIndex = StudyUtils.getIndex(taskDirName, Task.TASK_DIR);
388           List<Task> tasks = lesson.getTaskList();
389           if (!StudyUtils.indexIsValid(taskIndex, tasks)) {
390             return null;
391           }
392           Task task = tasks.get(taskIndex);
393           return task.getFile(file.getName());
394         }
395       }
396     }
397     return null;
398   }
399
400   class FileCreatedListener extends VirtualFileAdapter {
401     @Override
402     public void fileCreated(@NotNull VirtualFileEvent event) {
403       VirtualFile createdFile = event.getFile();
404       VirtualFile taskDir = createdFile.getParent();
405       String taskLogicalName = Task.TASK_DIR;
406       if (taskDir != null && taskDir.getName().contains(taskLogicalName)) {
407         int taskIndex = StudyUtils.getIndex(taskDir.getName(), taskLogicalName);
408         VirtualFile lessonDir = taskDir.getParent();
409         String lessonLogicalName = Lesson.LESSON_DIR;
410         if (lessonDir != null && lessonDir.getName().contains(lessonLogicalName)) {
411           int lessonIndex = StudyUtils.getIndex(lessonDir.getName(), lessonLogicalName);
412           if (myCourse != null) {
413             List<Lesson> lessons = myCourse.getLessons();
414             if (StudyUtils.indexIsValid(lessonIndex, lessons)) {
415               Lesson lesson = lessons.get(lessonIndex);
416               List<Task> tasks = lesson.getTaskList();
417               if (StudyUtils.indexIsValid(taskIndex, tasks)) {
418                 Task task = tasks.get(taskIndex);
419                 TaskFile taskFile = new TaskFile();
420                 taskFile.init(task, false);
421                 taskFile.setUserCreated(true);
422                 task.getTaskFiles().put(createdFile.getName(), taskFile);
423               }
424             }
425           }
426         }
427       }
428     }
429   }
430
431 }