Merge remote-tracking branch 'origin/master'
[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.impl.ProjectUtil;
5 import com.intellij.ide.ui.UISettings;
6 import com.intellij.ide.util.PropertiesComponent;
7 import com.intellij.notification.Notification;
8 import com.intellij.notification.NotificationListener;
9 import com.intellij.notification.NotificationType;
10 import com.intellij.notification.Notifications;
11 import com.intellij.openapi.actionSystem.*;
12 import com.intellij.openapi.actionSystem.ex.AnActionListener;
13 import com.intellij.openapi.application.ApplicationManager;
14 import com.intellij.openapi.application.PathManager;
15 import com.intellij.openapi.components.*;
16 import com.intellij.openapi.diagnostic.Logger;
17 import com.intellij.openapi.editor.EditorFactory;
18 import com.intellij.openapi.fileEditor.FileEditor;
19 import com.intellij.openapi.fileEditor.FileEditorManager;
20 import com.intellij.openapi.keymap.Keymap;
21 import com.intellij.openapi.keymap.KeymapManager;
22 import com.intellij.openapi.project.DumbAware;
23 import com.intellij.openapi.project.DumbAwareRunnable;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.startup.StartupManager;
26 import com.intellij.openapi.ui.popup.*;
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     final File pythonIntroduction = new File(ProjectUtil.getBaseDir(), "PythonIntroduction");
118     if (StudyInitialConfigurator.UPDATE_PROJECT && myProject.getBasePath().equals(pythonIntroduction.getAbsolutePath())) {
119       //noinspection AssignmentToStaticFieldFromInstanceMethod
120       StudyInitialConfigurator.UPDATE_PROJECT = false;
121       updateCourse();
122     }
123     ApplicationManager.getApplication().invokeLater(new DumbAwareRunnable() {
124       @Override
125       public void run() {
126         ApplicationManager.getApplication().runWriteAction(new DumbAwareRunnable() {
127           @Override
128           public void run() {
129             if (myCourse != null) {
130               StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() {
131                 @Override
132                 public void run() {
133                   ToolWindowManager.getInstance(myProject).getToolWindow(ToolWindowId.PROJECT_VIEW).show(new Runnable() {
134                     @Override
135                     public void run() {
136                       FileEditor[] editors = FileEditorManager.getInstance(myProject).getSelectedEditors();
137                       if (editors.length > 0) {
138                         final JComponent focusedComponent = editors[0].getPreferredFocusedComponent();
139                         if (focusedComponent != null) {
140                           ApplicationManager.getApplication().invokeLater(new Runnable() {
141                             @Override
142                             public void run() {
143                               IdeFocusManager.getInstance(myProject).requestFocus(focusedComponent, true);
144                             }
145                           });
146                         }
147                       }
148                     }
149                   });
150                 }
151               });
152               UISettings.getInstance().HIDE_TOOL_STRIPES = false;
153               UISettings.getInstance().fireUISettingsChanged();
154               ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(myProject);
155               String toolWindowId = StudyToolWindowFactory.STUDY_TOOL_WINDOW;
156               try {
157                 Method method = toolWindowManager.getClass().getDeclaredMethod("registerToolWindow", String.class,
158                                                                                JComponent.class,
159                                                                                ToolWindowAnchor.class,
160                                                                                boolean.class, boolean.class, boolean.class);
161                 method.setAccessible(true);
162                 method.invoke(toolWindowManager, toolWindowId, null, ToolWindowAnchor.LEFT, true, true, true);
163               }
164               catch (Exception e) {
165                 final ToolWindow toolWindow = toolWindowManager.getToolWindow(toolWindowId);
166                 if (toolWindow == null) {
167                   toolWindowManager.registerToolWindow(toolWindowId, true, ToolWindowAnchor.RIGHT, myProject, true);
168                 }
169               }
170
171               final ToolWindow studyToolWindow = toolWindowManager.getToolWindow(toolWindowId);
172               class UrlOpeningListener implements NotificationListener {
173                 private final boolean myExpireNotification;
174
175                 public UrlOpeningListener(boolean expireNotification) {
176                   myExpireNotification = expireNotification;
177                 }
178
179                 protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
180                   URL url = event.getURL();
181                   if (url == null) {
182                     BrowserUtil.browse(event.getDescription());
183                   }
184                   else {
185                     BrowserUtil.browse(url);
186                   }
187                   if (myExpireNotification) {
188                     notification.expire();
189                   }
190                 }
191
192                 @Override
193                 public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
194                   if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
195                     hyperlinkActivated(notification, event);
196                   }
197                 }
198               }
199               if (studyToolWindow != null) {
200                 StudyUtils.updateStudyToolWindow(myProject);
201                 studyToolWindow.show(null);
202                 UiNotifyConnector.doWhenFirstShown(studyToolWindow.getComponent(), new Runnable() {
203                   @Override
204                   public void run() {
205                     if (PropertiesComponent.getInstance().getBoolean("StudyShowPopup", true)) {
206                       String content = "<html>If you'd like to learn" +
207                                        " more about PyCharm " +
208                                        "Educational Edition, " +
209                                        "click <a href=\"https://www.jetbrains.com/pycharm-educational/quickstart/\">here</a> to watch a tutorial</html>";
210                       final Notification notification =
211                         new Notification("Watch Tutorials!", "", content, NotificationType.INFORMATION, new UrlOpeningListener(true));
212                       Notifications.Bus.notify(notification);
213                       Balloon balloon = notification.getBalloon();
214                       if (balloon != null) {
215                         balloon.addListener(new JBPopupAdapter() {
216                           @Override
217                           public void onClosed(LightweightWindowEvent event) {
218                             notification.expire();
219                           }
220                         });
221                       }
222                       notification.whenExpired(new Runnable() {
223                         @Override
224                         public void run() {
225                           PropertiesComponent.getInstance().setValue("StudyShowPopup", String.valueOf(false));
226                         }
227                       });
228                     }
229                   }
230                 });
231               }
232               addShortcut(StudyNextWindowAction.SHORTCUT, StudyNextWindowAction.ACTION_ID, false);
233               addShortcut(StudyPrevWindowAction.SHORTCUT, StudyPrevWindowAction.ACTION_ID, false);
234               addShortcut(StudyShowHintAction.SHORTCUT, StudyShowHintAction.ACTION_ID, false);
235               addShortcut(StudyNextWindowAction.SHORTCUT2, StudyNextWindowAction.ACTION_ID, true);
236               addShortcut(StudyCheckAction.SHORTCUT, StudyCheckAction.ACTION_ID, false);
237               addShortcut(StudyNextStudyTaskAction.SHORTCUT, StudyNextStudyTaskAction.ACTION_ID, false);
238               addShortcut(StudyPreviousStudyTaskAction.SHORTCUT, StudyPreviousStudyTaskAction.ACTION_ID, false);
239               addShortcut(StudyRefreshTaskFileAction.SHORTCUT, StudyRefreshTaskFileAction.ACTION_ID, false);
240             }
241           }
242         });
243       }
244     });
245   }
246
247   private void updateCourse() {
248     final File userCourseDir = new File(PathManager.getConfigPath(), StudyNames.COURSES);
249     final File courseDir = new File(userCourseDir, StudyNames.INTRODUCTION_COURSE);
250     final File[] files = courseDir.listFiles();
251     if (files == null) return;
252     for (File lesson : files) {
253       if (lesson.getName().startsWith(StudyNames.LESSON)) {
254         final File[] tasks = lesson.listFiles();
255         if (tasks == null) continue;
256         for (File task : tasks) {
257           final File taskDescr = new File(task, StudyNames.TASK_HTML);
258           final File taskTests = new File(task, StudyNames.TASK_TESTS);
259           copyFile(lesson, task, taskDescr, StudyNames.TASK_HTML);
260           copyFile(lesson, task, taskTests, StudyNames.TASK_TESTS);
261         }
262       }
263     }
264
265     final Notification notification =
266       new Notification("Update.course", "Course update", "Current course is synchronized", NotificationType.INFORMATION);
267     notification.notify(myProject);
268   }
269
270   private void copyFile(@NotNull final File lesson, @NotNull final File task, @NotNull final File taskDescr,
271                         @NotNull final String fileName) {
272     if (taskDescr.exists()) {
273       try {
274         FileUtil.copy(taskDescr, new File(new File(new File(myProject.getBasePath(), lesson.getName()), task.getName()), fileName));
275       }
276       catch (IOException e) {
277         LOG.warn("Failed to copy " + lesson.getName() + " " + task.getName());
278       }
279     }
280   }
281
282   private static void addShortcut(@NotNull final String shortcutString, @NotNull final String actionIdString, boolean isAdditional) {
283     Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
284     Shortcut[] shortcuts = keymap.getShortcuts(actionIdString);
285     if (shortcuts.length > 0 && !isAdditional) {
286       return;
287     }
288     Shortcut studyActionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcutString), null);
289     String[] actionsIds = keymap.getActionIds(studyActionShortcut);
290     for (String actionId : actionsIds) {
291       myDeletedShortcuts.put(actionId, shortcutString);
292       keymap.removeShortcut(actionId, studyActionShortcut);
293     }
294     keymap.addShortcut(actionIdString, studyActionShortcut);
295   }
296
297   @Override
298   public void projectClosed() {
299     StudyCondition.VALUE = false;
300     if (myCourse != null) {
301       ToolWindowManager.getInstance(myProject).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW).getContentManager()
302         .removeAllContents(false);
303       if (!myDeletedShortcuts.isEmpty()) {
304         for (Map.Entry<String, String> shortcut : myDeletedShortcuts.entrySet()) {
305           Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
306           Shortcut actionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcut.getValue()), null);
307           keymap.addShortcut(shortcut.getKey(), actionShortcut);
308         }
309       }
310     }
311   }
312
313   @Override
314   public void initComponent() {
315     EditorFactory.getInstance().addEditorFactoryListener(new StudyEditorFactoryListener(), myProject);
316     ActionManager.getInstance().addAnActionListener(new AnActionListener() {
317       @Override
318       public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
319         AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
320         for (AnAction newAction : newGroupActions) {
321           if (newAction == action) {
322             myListener =  new FileCreatedListener();
323             VirtualFileManager.getInstance().addVirtualFileListener(myListener);
324             break;
325           }
326         }
327       }
328
329       @Override
330       public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
331         AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
332         for (AnAction newAction : newGroupActions) {
333           if (newAction == action) {
334             VirtualFileManager.getInstance().removeVirtualFileListener(myListener);
335           }
336         }
337       }
338
339       @Override
340       public void beforeEditorTyping(char c, DataContext dataContext) {
341
342       }
343     });
344   }
345
346   @Override
347   public void disposeComponent() {
348   }
349
350   @NotNull
351   @Override
352   public String getComponentName() {
353     return "StudyTaskManager";
354   }
355
356   public static StudyTaskManager getInstance(@NotNull final Project project) {
357     StudyTaskManager item = myTaskManagers.get(project.getBasePath());
358     return item != null ? item : new StudyTaskManager(project);
359   }
360
361
362   @Nullable
363   public TaskFile getTaskFile(@NotNull final VirtualFile file) {
364     if (myCourse == null) {
365       return null;
366     }
367     VirtualFile taskDir = file.getParent();
368     if (taskDir != null) {
369       String taskDirName = taskDir.getName();
370       if (taskDirName.contains(Task.TASK_DIR)) {
371         VirtualFile lessonDir = taskDir.getParent();
372         if (lessonDir != null) {
373           String lessonDirName = lessonDir.getName();
374           int lessonIndex = StudyUtils.getIndex(lessonDirName, Lesson.LESSON_DIR);
375           List<Lesson> lessons = myCourse.getLessons();
376           if (!StudyUtils.indexIsValid(lessonIndex, lessons)) {
377             return null;
378           }
379           Lesson lesson = lessons.get(lessonIndex);
380           int taskIndex = StudyUtils.getIndex(taskDirName, Task.TASK_DIR);
381           List<Task> tasks = lesson.getTaskList();
382           if (!StudyUtils.indexIsValid(taskIndex, tasks)) {
383             return null;
384           }
385           Task task = tasks.get(taskIndex);
386           return task.getFile(file.getName());
387         }
388       }
389     }
390     return null;
391   }
392
393   class FileCreatedListener extends VirtualFileAdapter {
394     @Override
395     public void fileCreated(@NotNull VirtualFileEvent event) {
396       VirtualFile createdFile = event.getFile();
397       VirtualFile taskDir = createdFile.getParent();
398       String taskLogicalName = Task.TASK_DIR;
399       if (taskDir != null && taskDir.getName().contains(taskLogicalName)) {
400         int taskIndex = StudyUtils.getIndex(taskDir.getName(), taskLogicalName);
401         VirtualFile lessonDir = taskDir.getParent();
402         String lessonLogicalName = Lesson.LESSON_DIR;
403         if (lessonDir != null && lessonDir.getName().contains(lessonLogicalName)) {
404           int lessonIndex = StudyUtils.getIndex(lessonDir.getName(), lessonLogicalName);
405           if (myCourse != null) {
406             List<Lesson> lessons = myCourse.getLessons();
407             if (StudyUtils.indexIsValid(lessonIndex, lessons)) {
408               Lesson lesson = lessons.get(lessonIndex);
409               List<Task> tasks = lesson.getTaskList();
410               if (StudyUtils.indexIsValid(taskIndex, tasks)) {
411                 Task task = tasks.get(taskIndex);
412                 TaskFile taskFile = new TaskFile();
413                 taskFile.init(task, false);
414                 taskFile.setUserCreated(true);
415                 task.getTaskFiles().put(createdFile.getName(), taskFile);
416               }
417             }
418           }
419         }
420       }
421     }
422   }
423
424 }