summaryrefslogtreecommitdiff
path: root/python/edu/learn-python
diff options
context:
space:
mode:
Diffstat (limited to 'python/edu/learn-python')
-rw-r--r--python/edu/learn-python/gen/icons/StudyIcons.java29
-rw-r--r--python/edu/learn-python/learn-python.iml20
-rw-r--r--python/edu/learn-python/resources/META-INF/plugin.xml73
-rw-r--r--python/edu/learn-python/resources/com/jetbrains/python/edu/user_tester.py58
-rw-r--r--python/edu/learn-python/resources/courses/introduction_course.zipbin0 -> 93360 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/Run.pngbin0 -> 1260 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/WatchInput.pngbin0 -> 1328 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/add.pngbin0 -> 213 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/checked.pngbin0 -> 606 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/failed.pngbin0 -> 549 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/icon.jpgbin0 -> 4389 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/next.pngbin0 -> 1345 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/playground.pngbin0 -> 855 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/prev.pngbin0 -> 1354 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh.pngbin0 -> 657 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh24.pngbin0 -> 1569 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve.pngbin0 -> 342 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve_dark.pngbin0 -> 449 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/showHint.pngbin0 -> 1218 bytes
-rw-r--r--python/edu/learn-python/resources/icons/com/jetbrains/python/edu/unchecked.pngbin0 -> 604 bytes
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java393
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyDocumentListener.java65
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyEditorFactoryListener.java100
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyHighlightErrorFilter.java23
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyInitialConfigurator.java67
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyInstructionPainter.java47
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyResourceManger.java5
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyTaskManager.java296
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/StudyUtils.java151
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyCheckAction.java340
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyEditInputAction.java213
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextStudyTaskAction.java24
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextWindowAction.java32
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPrevWindowAction.java34
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPreviousStudyTaskAction.java25
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskAction.java122
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRunAction.java89
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyShowHintAction.java95
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java97
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyWindowNavigationAction.java65
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/Course.java104
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/CourseInfo.java52
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/Lesson.java109
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/LessonInfo.java60
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/Stateful.java6
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/StudyStatus.java8
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/Task.java201
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskFile.java228
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskWindow.java177
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/UserTest.java41
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyEditor.java347
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyFileEditorProvider.java64
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java112
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyTreeStructureProvider.java83
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyCondition.java25
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.form84
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.java196
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyProgressBar.java92
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyTestContentPanel.java67
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java81
-rw-r--r--python/edu/learn-python/testData/course.json130
-rw-r--r--python/edu/learn-python/tests/JsonParserTest.java37
62 files changed, 4767 insertions, 0 deletions
diff --git a/python/edu/learn-python/gen/icons/StudyIcons.java b/python/edu/learn-python/gen/icons/StudyIcons.java
new file mode 100644
index 000000000000..28409103ea00
--- /dev/null
+++ b/python/edu/learn-python/gen/icons/StudyIcons.java
@@ -0,0 +1,29 @@
+package icons;
+
+import com.intellij.openapi.util.IconLoader;
+
+import javax.swing.*;
+
+/**
+ * NOTE THIS FILE IS AUTO-GENERATED
+ * DO NOT EDIT IT BY HAND, run build/scripts/icons.gant instead
+ */
+public class StudyIcons {
+ private static Icon load(String path) {
+ return IconLoader.getIcon(path, StudyIcons.class);
+ }
+
+ public static final Icon Add = load("/icons/com/jetbrains/python/edu/add.png"); // 16x16
+ public static final Icon Checked = load("/icons/com/jetbrains/python/edu/checked.png"); // 32x32
+ public static final Icon Failed = load("/icons/com/jetbrains/python/edu/failed.png"); // 32x32
+ public static final Icon Next = load("/icons/com/jetbrains/python/edu/next.png"); // 24x24
+ public static final Icon Playground = load("/icons/com/jetbrains/python/edu/playground.png"); // 32x28
+ public static final Icon Prev = load("/icons/com/jetbrains/python/edu/prev.png"); // 24x24
+ public static final Icon Refresh = load("/icons/com/jetbrains/python/edu/refresh.png"); // 16x16
+ public static final Icon Refresh24 = load("/icons/com/jetbrains/python/edu/refresh24.png"); // 24x24
+ public static final Icon Resolve = load("/icons/com/jetbrains/python/edu/resolve.png"); // 24x24
+ public static final Icon Run = load("/icons/com/jetbrains/python/edu/Run.png"); // 24x24
+ public static final Icon ShowHint = load("/icons/com/jetbrains/python/edu/showHint.png"); // 24x24
+ public static final Icon Unchecked = load("/icons/com/jetbrains/python/edu/unchecked.png"); // 32x32
+ public static final Icon WatchInput = load("/icons/com/jetbrains/python/edu/WatchInput.png"); // 24x24
+}
diff --git a/python/edu/learn-python/learn-python.iml b/python/edu/learn-python/learn-python.iml
new file mode 100644
index 000000000000..bd539d1e2147
--- /dev/null
+++ b/python/edu/learn-python/learn-python.iml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="module" module-name="platform-impl" />
+ <orderEntry type="module" module-name="python-community" />
+ <orderEntry type="module" module-name="lang-impl" />
+ <orderEntry type="library" name="gson" level="project" />
+ <orderEntry type="library" name="JUnit4" level="project" />
+ </component>
+</module>
+
diff --git a/python/edu/learn-python/resources/META-INF/plugin.xml b/python/edu/learn-python/resources/META-INF/plugin.xml
new file mode 100644
index 000000000000..ec828eb44d59
--- /dev/null
+++ b/python/edu/learn-python/resources/META-INF/plugin.xml
@@ -0,0 +1,73 @@
+<!--suppress XmlUnboundNsPrefix -->
+<idea-plugin version="2">
+ <id>com.jetbrains.python.edu.learn-python</id>
+ <name>Educational plugin for PyCharm</name>
+ <version>1.0</version>
+ <vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>
+
+ <description><![CDATA[
+
+ ]]></description>
+
+ <change-notes><![CDATA[
+
+ ]]>
+ </change-notes>
+
+ <!--depends>com.intellij.modules.python</depends-->
+
+ <!-- please see http://confluence.jetbrains.net/display/IDEADEV/Plugin+Compatibility+with+IntelliJ+Platform+Products
+ on how to target different products -->
+
+ <depends>com.intellij.modules.lang</depends>
+ <depends>com.intellij.modules.python</depends>
+ <application-components>
+ </application-components>
+
+ <project-components>
+ <component>
+ <implementation-class>com.jetbrains.python.edu.StudyTaskManager</implementation-class>
+ <interface-class>com.jetbrains.python.edu.StudyTaskManager</interface-class>
+ </component>
+ </project-components>
+
+ <application-components>
+ <component>
+ <implementation-class>com.jetbrains.python.edu.StudyInitialConfigurator</implementation-class>
+ <headless-implementation-class/>
+ </component>
+ </application-components>
+
+ <actions>
+ <action id="CheckAction" class="com.jetbrains.python.edu.actions.StudyCheckAction" text="check"
+ description="Runs tests for current tasks" icon="/icons/icon.jpg">
+ </action>
+ <action id="PrevWindowAction" class="com.jetbrains.python.edu.actions.StudyPrevWindowAction" text="PrevWindowAction" description="prev">
+ </action>
+
+ <action id="NextWindow" class="com.jetbrains.python.edu.actions.StudyNextWindowAction" text="NextWindowAction" description="next">
+ </action>
+ <action id="NextTaskAction" class="com.jetbrains.python.edu.actions.StudyNextStudyTaskAction" text="NextTaskAction" description="Next Task"/>
+ <action id="PreviousTaskAction" class="com.jetbrains.python.edu.actions.StudyPreviousStudyTaskAction" text="PreviousTaskAction"
+ description="Previous Task"/>
+ <action id="RefreshTaskAction" class="com.jetbrains.python.edu.actions.StudyRefreshTaskAction" text="RefreshTaskAction"
+ description="Refresh current task"/>
+ <action id="WatchInputAction" class="com.jetbrains.python.edu.actions.StudyEditInputAction" text="WatchInputAction"
+ description="watch input"/>
+ <action id="StudyRunAction" class="com.jetbrains.python.edu.actions.StudyRunAction" text="StudyRunAction" description="run your code"/>
+ <action id="ShowHintAction" class="com.jetbrains.python.edu.actions.StudyShowHintAction" text="Show hint"
+ description="show hint">
+ <add-to-group group-id="MainToolBar" anchor="last"/>
+ </action>
+ </actions>
+
+ <extensions defaultExtensionNs="com.intellij">
+ <toolWindow id="Course Description" anchor="right" factoryClass="com.jetbrains.python.edu.ui.StudyToolWindowFactory" conditionClass="com.jetbrains.python.edu.ui.StudyCondition"/>
+ <fileEditorProvider implementation="com.jetbrains.python.edu.editor.StudyFileEditorProvider"/>
+ <directoryProjectGenerator implementation="com.jetbrains.python.edu.StudyDirectoryProjectGenerator"/>
+ <treeStructureProvider implementation="com.jetbrains.python.edu.projectView.StudyTreeStructureProvider"/>
+ <highlightErrorFilter implementation="com.jetbrains.python.edu.StudyHighlightErrorFilter"/>
+ <applicationService serviceInterface="com.intellij.openapi.fileEditor.impl.EditorEmptyTextPainter"
+ serviceImplementation="com.jetbrains.python.edu.StudyInstructionPainter" overrides="true"/>
+ </extensions>
+</idea-plugin> \ No newline at end of file
diff --git a/python/edu/learn-python/resources/com/jetbrains/python/edu/user_tester.py b/python/edu/learn-python/resources/com/jetbrains/python/edu/user_tester.py
new file mode 100644
index 000000000000..c17e6cdd7479
--- /dev/null
+++ b/python/edu/learn-python/resources/com/jetbrains/python/edu/user_tester.py
@@ -0,0 +1,58 @@
+import sys
+import imp
+import os
+import subprocess
+
+USER_TESTS = "userTests"
+
+TEST_FAILED = "FAILED"
+
+TEST_PASSED = "PASSED"
+
+INPUT = "input"
+OUTPUT = "output"
+
+
+def get_index(logical_name, full_name):
+ logical_name_len = len(logical_name)
+ if full_name[:logical_name_len] == logical_name:
+ return int(full_name[logical_name_len])
+ return -1
+
+
+def process_user_tests(file_path):
+ user_tests = []
+ imp.load_source('user_file', file_path)
+ user_tests_dir_path = os.path.abspath(os.path.join(file_path, os.pardir, USER_TESTS))
+ user_test_files = os.listdir(user_tests_dir_path)
+ for user_file in user_test_files:
+ index = get_index(INPUT, user_file)
+ if index == -1:
+ continue
+ output = OUTPUT + str(index)
+ if output in user_test_files:
+ input_path = os.path.abspath(os.path.join(user_tests_dir_path, user_file))
+ output_path = os.path.abspath(os.path.join(user_tests_dir_path, output))
+ user_tests.append((input_path, output_path, index))
+ return sorted(user_tests, key=(lambda x: x[2]))
+
+
+def run_user_test(python, executable_path):
+ user_tests = process_user_tests(executable_path)
+ for test in user_tests:
+ input, output, index = test
+ test_output = subprocess.check_output([python, executable_path, input])
+ expected_output = open(output).read()
+ test_status = TEST_PASSED if test_output == expected_output else TEST_FAILED
+ print "TEST" + str(index) + " " + test_status
+ print "OUTPUT:"
+ print test_output + "\n"
+ if test_status == TEST_FAILED:
+ print "EXPECTED OUTPUT:"
+ print expected_output + "\n"
+
+
+if __name__ == "__main__":
+ python = sys.argv[1]
+ executable_path = sys.argv[2]
+ run_user_test(python , executable_path) \ No newline at end of file
diff --git a/python/edu/learn-python/resources/courses/introduction_course.zip b/python/edu/learn-python/resources/courses/introduction_course.zip
new file mode 100644
index 000000000000..f3b24f24b1e2
--- /dev/null
+++ b/python/edu/learn-python/resources/courses/introduction_course.zip
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/Run.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/Run.png
new file mode 100644
index 000000000000..27a6e362cded
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/Run.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/WatchInput.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/WatchInput.png
new file mode 100644
index 000000000000..4992191eb9e7
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/WatchInput.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/add.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/add.png
new file mode 100644
index 000000000000..9494f2d0c72e
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/add.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/checked.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/checked.png
new file mode 100644
index 000000000000..4105a01f1353
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/checked.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/failed.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/failed.png
new file mode 100644
index 000000000000..e2aaa556056e
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/failed.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/icon.jpg b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/icon.jpg
new file mode 100644
index 000000000000..3a9716e4e1fb
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/icon.jpg
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/next.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/next.png
new file mode 100644
index 000000000000..dd1a5d9aebf3
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/next.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/playground.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/playground.png
new file mode 100644
index 000000000000..d12a751c0c40
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/playground.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/prev.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/prev.png
new file mode 100644
index 000000000000..0656f81eee83
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/prev.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh.png
new file mode 100644
index 000000000000..d595f6b42f56
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh24.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh24.png
new file mode 100644
index 000000000000..218f075d0c18
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh24.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve.png
new file mode 100644
index 000000000000..7ef960bcf244
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve_dark.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve_dark.png
new file mode 100644
index 000000000000..99aaa1d20a32
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve_dark.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/showHint.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/showHint.png
new file mode 100644
index 000000000000..f10fd560464a
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/showHint.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/unchecked.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/unchecked.png
new file mode 100644
index 000000000000..2145982cf2be
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/unchecked.png
Binary files differ
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java
new file mode 100644
index 000000000000..d4831d93e367
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java
@@ -0,0 +1,393 @@
+package com.jetbrains.python.edu;
+
+import com.google.gson.*;
+import com.google.gson.stream.JsonReader;
+import com.intellij.facet.ui.FacetEditorValidator;
+import com.intellij.facet.ui.FacetValidatorsManager;
+import com.intellij.facet.ui.ValidationResult;
+import com.intellij.lang.javascript.boilerplate.GithubDownloadUtil;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.platform.DirectoryProjectGenerator;
+import com.intellij.platform.templates.github.GeneratorException;
+import com.intellij.platform.templates.github.ZipUtil;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.CourseInfo;
+import com.jetbrains.python.edu.ui.StudyNewProjectPanel;
+import com.jetbrains.python.newProject.PythonProjectGenerator;
+import icons.StudyIcons;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class StudyDirectoryProjectGenerator extends PythonProjectGenerator implements DirectoryProjectGenerator {
+ private static final Logger LOG = Logger.getInstance(StudyDirectoryProjectGenerator.class.getName());
+ private static final String REPO_URL = "https://github.com/JetBrains/pycharm-courses/archive/master.zip";
+ private static final String USER_NAME = "PyCharm";
+ private static final String COURSE_META_FILE = "course.json";
+ private static final String COURSE_NAME_ATTRIBUTE = "name";
+ private static final Pattern CACHE_PATTERN = Pattern.compile("(name=(.*)) (path=(.*course.json)) (author=(.*)) (description=(.*))");
+ private static final String REPOSITORY_NAME = "pycharm-courses";
+ public static final String AUTHOR_ATTRIBUTE = "author";
+ private final File myCoursesDir = new File(PathManager.getConfigPath(), "courses");
+ private static final String CACHE_NAME = "courseNames.txt";
+ private Map<CourseInfo, File> myCourses = new HashMap<CourseInfo, File>();
+ private File mySelectedCourseFile;
+ private Project myProject;
+ public ValidationResult myValidationResult = new ValidationResult("selected course is not valid");
+
+ @Nls
+ @NotNull
+ @Override
+ public String getName() {
+ return "Study project";
+ }
+
+
+ public void setCourses(Map<CourseInfo, File> courses) {
+ myCourses = courses;
+ }
+
+ /**
+ * Finds selected course in courses by name.
+ *
+ * @param courseName name of selected course
+ */
+ public void setSelectedCourse(@NotNull CourseInfo courseName) {
+ File courseFile = myCourses.get(courseName);
+ if (courseFile == null) {
+ LOG.error("invalid course in list");
+ }
+ mySelectedCourseFile = courseFile;
+ }
+
+ /**
+ * Adds course to courses specified in params
+ *
+ * @param courseDir must be directory containing course file
+ * @return added course name or null if course is invalid
+ */
+ @Nullable
+ private static CourseInfo addCourse(Map<CourseInfo, File> courses, File courseDir) {
+ if (courseDir.isDirectory()) {
+ File[] courseFiles = courseDir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.equals(COURSE_META_FILE);
+ }
+ });
+ if (courseFiles.length != 1) {
+ LOG.info("User tried to add course with more than one or without course files");
+ return null;
+ }
+ File courseFile = courseFiles[0];
+ CourseInfo courseInfo = getCourseInfo(courseFile);
+ if (courseInfo != null) {
+ courses.put(courseInfo, courseFile);
+ }
+ return courseInfo;
+ }
+ return null;
+ }
+
+
+ /**
+ * Adds course from zip archive to courses
+ *
+ * @return added course name or null if course is invalid
+ */
+ @Nullable
+ public CourseInfo addLocalCourse(String zipFilePath) {
+ File file = new File(zipFilePath);
+ try {
+ String fileName = file.getName();
+ String unzippedName = fileName.substring(0, fileName.indexOf("."));
+ File courseDir = new File(myCoursesDir, unzippedName);
+ ZipUtil.unzip(null, courseDir, file, null, null, true);
+ CourseInfo courseName = addCourse(myCourses, courseDir);
+ flushCache();
+ return courseName;
+ }
+ catch (IOException e) {
+ LOG.error("Failed to unzip course archive");
+ LOG.error(e);
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Object showGenerationSettings(VirtualFile baseDir) throws ProcessCanceledException {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Icon getLogo() {
+ return StudyIcons.Playground;
+ }
+
+
+ @Override
+ public void generateProject(@NotNull final Project project, @NotNull final VirtualFile baseDir,
+ @Nullable Object settings, @NotNull Module module) {
+
+ myProject = project;
+ Reader reader = null;
+ try {
+ reader = new InputStreamReader(new FileInputStream(mySelectedCourseFile));
+ Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+ Course course = gson.fromJson(reader, Course.class);
+ course.init(false);
+ course.create(baseDir, new File(mySelectedCourseFile.getParent()));
+ course.setResourcePath(mySelectedCourseFile.getAbsolutePath());
+ VirtualFileManager.getInstance().refreshWithoutFileWatcher(true);
+ StudyTaskManager.getInstance(project).setCourse(course);
+ }
+ catch (FileNotFoundException e) {
+ LOG.error(e);
+ }
+ finally {
+ StudyUtils.closeSilently(reader);
+ }
+ }
+
+ /**
+ * Downloads courses from {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#REPO_URL}
+ * and unzips them into {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCoursesDir}
+ */
+
+ public void downloadAndUnzip(boolean needProgressBar) {
+ File outputFile = new File(PathManager.getConfigPath(), "courses.zip");
+ try {
+ if (!needProgressBar) {
+ GithubDownloadUtil.downloadAtomically(null, REPO_URL,
+ outputFile, USER_NAME, REPOSITORY_NAME);
+ }
+ else {
+ GithubDownloadUtil.downloadContentToFileWithProgressSynchronously(myProject, REPO_URL, "downloading courses", outputFile, USER_NAME,
+ REPOSITORY_NAME, false);
+ }
+ if (outputFile.exists()) {
+ ZipUtil.unzip(null, myCoursesDir, outputFile, null, null, true);
+ if (!outputFile.delete()) {
+ LOG.error("Failed to delete", outputFile.getName());
+ }
+ File[] files = myCoursesDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ String fileName = file.getName();
+ if (StudyUtils.isZip(fileName)) {
+ ZipUtil.unzip(null, new File(myCoursesDir, fileName.substring(0, fileName.indexOf("."))), file, null, null, true);
+ if (!file.delete()) {
+ LOG.error("Failed to delete", fileName);
+ }
+ }
+ }
+ }
+ } else {
+ LOG.debug("failed to download course");
+ }
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ catch (GeneratorException e) {
+ LOG.error(e);
+ }
+ }
+
+ public Map<CourseInfo, File> getLoadedCourses() {
+ return myCourses;
+ }
+
+ /**
+ * Parses courses located in {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCoursesDir}
+ * to {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCourses}
+ *
+ * @return map with course names and course files location
+ */
+ public Map<CourseInfo, File> loadCourses() {
+ Map<CourseInfo, File> courses = new HashMap<CourseInfo, File>();
+ if (myCoursesDir.exists()) {
+ File[] courseDirs = myCoursesDir.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return pathname.isDirectory();
+ }
+ });
+ for (File courseDir : courseDirs) {
+ addCourse(courses, courseDir);
+ }
+ }
+ return courses;
+ }
+
+ /**
+ * Parses course json meta file and finds course name
+ *
+ * @return information about course or null if course file is invalid
+ */
+ @Nullable
+ private static CourseInfo getCourseInfo(File courseFile) {
+ CourseInfo courseInfo = null;
+ BufferedReader reader = null;
+ try {
+ if (courseFile.getName().equals(COURSE_META_FILE)) {
+ reader = new BufferedReader(new InputStreamReader(new FileInputStream(courseFile)));
+ JsonReader r = new JsonReader(reader);
+ JsonParser parser = new JsonParser();
+ JsonElement el = parser.parse(r);
+ String courseName = el.getAsJsonObject().get(COURSE_NAME_ATTRIBUTE).getAsString();
+ String courseAuthor = el.getAsJsonObject().get(AUTHOR_ATTRIBUTE).getAsString();
+ String courseDescription = el.getAsJsonObject().get("description").getAsString();
+ courseInfo = new CourseInfo(courseName, courseAuthor, courseDescription);
+ }
+ }
+ catch (Exception e) {
+ //error will be shown in UI
+ }
+ finally {
+ StudyUtils.closeSilently(reader);
+ }
+ return courseInfo;
+ }
+
+ @NotNull
+ @Override
+ public ValidationResult validate(@NotNull String s) {
+ return myValidationResult;
+ }
+
+ public void setValidationResult(ValidationResult validationResult) {
+ myValidationResult = validationResult;
+ }
+
+ /**
+ * @return courses from memory or from cash file or parses course directory
+ */
+ public Map<CourseInfo, File> getCourses() {
+ if (!myCourses.isEmpty()) {
+ return myCourses;
+ }
+ if (myCoursesDir.exists()) {
+ File cacheFile = new File(myCoursesDir, CACHE_NAME);
+ if (cacheFile.exists()) {
+ myCourses = getCoursesFromCache(cacheFile);
+ if (!myCourses.isEmpty()) {
+ return myCourses;
+ }
+ }
+ myCourses = loadCourses();
+ if (!myCourses.isEmpty()) {
+ return myCourses;
+ }
+ }
+ downloadAndUnzip(false);
+ myCourses = loadCourses();
+ flushCache();
+ return myCourses;
+ }
+
+ /**
+ * Writes courses to cash file {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#CACHE_NAME}
+ */
+ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+ public void flushCache() {
+ File cashFile = new File(myCoursesDir, CACHE_NAME);
+ PrintWriter writer = null;
+ try {
+ if (!cashFile.exists()) {
+ final boolean created = cashFile.createNewFile();
+ if (!created) {
+ LOG.error("Cannot flush courses cache. Can't create " + CACHE_NAME + " file");
+ return;
+ }
+ }
+ writer = new PrintWriter(cashFile);
+ for (Map.Entry<CourseInfo, File> course : myCourses.entrySet()) {
+ CourseInfo courseInfo = course.getKey();
+ String line = String
+ .format("name=%s path=%s author=%s description=%s", courseInfo.getName(), course.getValue(), courseInfo.getAuthor(),
+ courseInfo.getDescription());
+ writer.println(line);
+ }
+ }
+ catch (FileNotFoundException e) {
+ LOG.error(e);
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ finally {
+ StudyUtils.closeSilently(writer);
+ }
+ }
+
+ /**
+ * Loads courses from {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#CACHE_NAME} file
+ *
+ * @return map of course names and course files
+ */
+ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+ private static Map<CourseInfo, File> getCoursesFromCache(File cashFile) {
+ Map<CourseInfo, File> coursesFromCash = new HashMap<CourseInfo, File>();
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new InputStreamReader(new FileInputStream(cashFile)));
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+ Matcher matcher = CACHE_PATTERN.matcher(line);
+ if (matcher.matches()) {
+ String courseName = matcher.group(2);
+ File file = new File(matcher.group(4));
+ String author = matcher.group(6);
+ String description = matcher.group(8);
+ CourseInfo courseInfo = new CourseInfo(courseName, author, description);
+ if (file.exists()) {
+ coursesFromCash.put(courseInfo, file);
+ }
+ }
+ }
+ }
+ catch (FileNotFoundException e) {
+ LOG.error(e);
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ finally {
+ StudyUtils.closeSilently(reader);
+ }
+ return coursesFromCash;
+ }
+
+ @Nullable
+ @Override
+ public JPanel extendBasePanel() throws ProcessCanceledException {
+ StudyNewProjectPanel settingsPanel = new StudyNewProjectPanel(this);
+ settingsPanel.registerValidators(new FacetValidatorsManager() {
+ public void registerValidator(FacetEditorValidator validator, JComponent... componentsToWatch) {
+ throw new UnsupportedOperationException();
+ }
+ public void validate() {
+ fireStateChanged();
+ }
+ });
+ return settingsPanel.getContentPanel();
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDocumentListener.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDocumentListener.java
new file mode 100644
index 000000000000..9fdcf704a29b
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDocumentListener.java
@@ -0,0 +1,65 @@
+package com.jetbrains.python.edu;
+
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.editor.event.DocumentAdapter;
+import com.intellij.openapi.editor.event.DocumentEvent;
+import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+
+/**
+ * author: liana
+ * data: 7/16/14.
+ * Listens changes in study files and updates
+ * coordinates of all the windows in current task file
+ */
+public class StudyDocumentListener extends DocumentAdapter {
+ private final TaskFile myTaskFile;
+ private int oldLine;
+ private int oldLineStartOffset;
+ private TaskWindow myTaskWindow;
+
+ public StudyDocumentListener(TaskFile taskFile) {
+ myTaskFile = taskFile;
+ }
+
+
+ //remembering old end before document change because of problems
+ // with fragments containing "\n"
+ @Override
+ public void beforeDocumentChange(DocumentEvent e) {
+ int offset = e.getOffset();
+ int oldEnd = offset + e.getOldLength();
+ Document document = e.getDocument();
+ oldLine = document.getLineNumber(oldEnd);
+ oldLineStartOffset = document.getLineStartOffset(oldLine);
+ int line = document.getLineNumber(offset);
+ int offsetInLine = offset - document.getLineStartOffset(line);
+ LogicalPosition pos = new LogicalPosition(line, offsetInLine);
+ myTaskWindow = myTaskFile.getTaskWindow(document, pos);
+
+ }
+
+ @Override
+ public void documentChanged(DocumentEvent e) {
+ if (e instanceof DocumentEventImpl) {
+ DocumentEventImpl event = (DocumentEventImpl)e;
+ Document document = e.getDocument();
+ int offset = e.getOffset();
+ int change = event.getNewLength() - event.getOldLength();
+ if (myTaskWindow != null) {
+ int newLength = myTaskWindow.getLength() + change;
+ myTaskWindow.setLength(newLength <= 0 ? 0 : newLength);
+ }
+ int newEnd = offset + event.getNewLength();
+ int newLine = document.getLineNumber(newEnd);
+ int lineChange = newLine - oldLine;
+ myTaskFile.incrementLines(oldLine + 1, lineChange);
+ int newEndOffsetInLine = offset + e.getNewLength() - document.getLineStartOffset(newLine);
+ int oldEndOffsetInLine = offset + e.getOldLength() - oldLineStartOffset;
+ myTaskFile.updateLine(lineChange, oldLine, newEndOffsetInLine, oldEndOffsetInLine);
+ }
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyEditorFactoryListener.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyEditorFactoryListener.java
new file mode 100644
index 000000000000..7565d76b3acf
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyEditorFactoryListener.java
@@ -0,0 +1,100 @@
+package com.jetbrains.python.edu;
+
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.editor.event.EditorFactoryEvent;
+import com.intellij.openapi.editor.event.EditorFactoryListener;
+import com.intellij.openapi.editor.event.EditorMouseAdapter;
+import com.intellij.openapi.editor.event.EditorMouseEvent;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.edu.course.StudyStatus;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import org.jetbrains.annotations.NotNull;
+
+import java.awt.*;
+
+
+class StudyEditorFactoryListener implements EditorFactoryListener {
+
+ /**
+ * draws selected task window if there is one located in mouse position
+ */
+ private static class WindowSelectionListener extends EditorMouseAdapter {
+ private final TaskFile myTaskFile;
+
+ WindowSelectionListener(TaskFile taskFile) {
+ myTaskFile = taskFile;
+ }
+
+ @Override
+ public void mouseClicked(EditorMouseEvent e) {
+ Editor editor = e.getEditor();
+ Point point = e.getMouseEvent().getPoint();
+ LogicalPosition pos = editor.xyToLogicalPosition(point);
+ TaskWindow taskWindow = myTaskFile.getTaskWindow(editor.getDocument(), pos);
+ if (taskWindow != null) {
+ myTaskFile.setSelectedTaskWindow(taskWindow);
+ taskWindow.draw(editor, taskWindow.getStatus() != StudyStatus.Solved, true);
+ }
+ else {
+ myTaskFile.drawAllWindows(editor);
+ }
+ }
+ }
+
+ @Override
+ public void editorCreated(@NotNull final EditorFactoryEvent event) {
+ final Editor editor = event.getEditor();
+
+ final Project project = editor.getProject();
+ if (project == null) {
+ return;
+ }
+ ApplicationManager.getApplication().invokeLater(
+ new Runnable() {
+ @Override
+ public void run() {
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ Document document = editor.getDocument();
+ VirtualFile openedFile = FileDocumentManager.getInstance().getFile(document);
+ if (openedFile != null) {
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ TaskFile taskFile = taskManager.getTaskFile(openedFile);
+ if (taskFile != null) {
+ taskFile.navigateToFirstTaskWindow(editor);
+ editor.addEditorMouseListener(new WindowSelectionListener(taskFile));
+ StudyDocumentListener listener = new StudyDocumentListener(taskFile);
+ StudyEditor.addDocumentListener(document, listener);
+ document.addDocumentListener(listener);
+ taskFile.drawAllWindows(editor);
+ }
+ }
+ }
+ });
+ }
+ }
+ );
+ }
+
+ @Override
+ public void editorReleased(@NotNull EditorFactoryEvent event) {
+ Editor editor = event.getEditor();
+ Document document = editor.getDocument();
+ StudyDocumentListener listener = StudyEditor.getListener(document);
+ if (listener != null) {
+ document.removeDocumentListener(listener);
+ StudyEditor.removeListener(document);
+ }
+ editor.getMarkupModel().removeAllHighlighters();
+ editor.getSelectionModel().removeSelection();
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyHighlightErrorFilter.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyHighlightErrorFilter.java
new file mode 100644
index 000000000000..1377128ed417
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyHighlightErrorFilter.java
@@ -0,0 +1,23 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.codeInsight.highlighting.HighlightErrorFilter;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiErrorElement;
+import org.jetbrains.annotations.NotNull;
+import com.jetbrains.python.edu.course.TaskFile;
+
+/**
+ * author: liana
+ * data: 7/23/14.
+ */
+public class StudyHighlightErrorFilter extends HighlightErrorFilter {
+ @Override
+ public boolean shouldHighlightErrorElement(@NotNull final PsiErrorElement element) {
+ VirtualFile file = element.getContainingFile().getVirtualFile();
+ Project project = element.getProject();
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ TaskFile taskFile = taskManager.getTaskFile(file);
+ return taskFile == null;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInitialConfigurator.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInitialConfigurator.java
new file mode 100644
index 000000000000..34776f3fabd2
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInitialConfigurator.java
@@ -0,0 +1,67 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.codeInsight.CodeInsightSettings;
+import com.intellij.ide.RecentProjectsManagerBase;
+import com.intellij.ide.ui.UISettings;
+import com.intellij.ide.util.PropertiesComponent;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileTypes.FileTypeManager;
+import com.intellij.openapi.project.ex.ProjectManagerEx;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.platform.templates.github.ZipUtil;
+import com.intellij.util.PathUtil;
+import com.intellij.util.messages.MessageBus;
+import org.jetbrains.annotations.NonNls;
+
+import java.io.File;
+import java.io.IOException;
+
+@SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "UtilityClassWithPublicConstructor"})
+public class StudyInitialConfigurator {
+ private static final Logger LOG = Logger.getInstance(StudyInitialConfigurator.class.getName()
+ );
+ @NonNls private static final String CONFIGURED = "StudyPyCharm.InitialConfiguration";
+
+
+ /**
+ * @noinspection UnusedParameters
+ */
+ public StudyInitialConfigurator(MessageBus bus,
+ UISettings uiSettings,
+ CodeInsightSettings codeInsightSettings,
+ final PropertiesComponent propertiesComponent,
+ FileTypeManager fileTypeManager,
+ final ProjectManagerEx projectManager,
+ RecentProjectsManagerBase recentProjectsManager) {
+ if (!propertiesComponent.getBoolean(CONFIGURED, false)) {
+ final File file = new File(getCoursesRoot(), "introduction_course.zip");
+ final File newCourses = new File(PathManager.getConfigPath(), "courses");
+ try {
+ FileUtil.createDirectory(newCourses);
+ String fileName = file.getName();
+ String unzippedName = fileName.substring(0, fileName.indexOf("."));
+ File courseDir = new File(newCourses, unzippedName);
+ ZipUtil.unzip(null, courseDir, file, null, null, true);
+
+ }
+ catch (IOException e) {
+ LOG.warn("Couldn't copy bundled courses " + e);
+ }
+ }
+ }
+
+ public static File getCoursesRoot() {
+ @NonNls String jarPath = PathUtil.getJarPathForClass(StudyInitialConfigurator.class);
+ if (jarPath.endsWith(".jar")) {
+ final File jarFile = new File(jarPath);
+
+
+ File pluginBaseDir = jarFile.getParentFile();
+ return new File(pluginBaseDir, "courses");
+ }
+
+ return new File(jarPath , "courses");
+ }
+
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInstructionPainter.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInstructionPainter.java
new file mode 100644
index 000000000000..4f34bfb92fab
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInstructionPainter.java
@@ -0,0 +1,47 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.openapi.fileEditor.impl.EditorEmptyTextPainter;
+import com.intellij.openapi.fileEditor.impl.EditorsSplitters;
+import com.intellij.openapi.util.Couple;
+import com.intellij.ui.Gray;
+import com.intellij.ui.JBColor;
+import com.intellij.util.PairFunction;
+import com.intellij.util.ui.GraphicsUtil;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.python.edu.ui.StudyCondition;
+
+import java.awt.*;
+
+/**
+ * author: liana
+ * data: 7/29/14.
+ */
+public class StudyInstructionPainter extends EditorEmptyTextPainter {
+ @Override
+ public void paintEmptyText(final EditorsSplitters splitters, Graphics g) {
+ if (!StudyCondition.VALUE) {
+ super.paintEmptyText(splitters, g);
+ return;
+ }
+ boolean isDarkBackground = UIUtil.isUnderDarcula();
+ UIUtil.applyRenderingHints(g);
+ GraphicsUtil.setupAntialiasing(g, true, false);
+ g.setColor(new JBColor(isDarkBackground ? Gray._230 : Gray._80, Gray._160));
+ g.setFont(UIUtil.getLabelFont().deriveFont(isDarkBackground ? 24f : 20f));
+
+ UIUtil.TextPainter painter = new UIUtil.TextPainter().withLineSpacing(1.5f);
+
+ painter.appendLine("PyCharm Educational Edition").underlined(new JBColor(Gray._150, Gray._180));
+ painter.appendLine("Navigate to the next task window with Ctrl + Enter").smaller().withBullet();
+ painter.appendLine("Navigate between task windows with Ctrl + < and Ctrl + >").smaller().withBullet();
+ painter.appendLine("Get hint for the task window using Ctrl + 7").smaller().withBullet();
+ painter.appendLine("To see your progress open the 'Course Description' panel").smaller().withBullet();
+ painter.draw(g, new PairFunction<Integer, Integer, Couple<Integer>>() {
+ @Override
+ public Couple<Integer> fun(Integer width, Integer height) {
+ Dimension s = splitters.getSize();
+ return Couple.of((s.width - width) / 2, (s.height - height) / 2);
+ }
+ });
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyResourceManger.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyResourceManger.java
new file mode 100644
index 000000000000..38f1c2ff2cde
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyResourceManger.java
@@ -0,0 +1,5 @@
+package com.jetbrains.python.edu;
+
+public interface StudyResourceManger {
+ String USER_TESTER = "user_tester.py";
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTaskManager.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTaskManager.java
new file mode 100644
index 000000000000..213c1f7601f0
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTaskManager.java
@@ -0,0 +1,296 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.ide.ui.UISettings;
+import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.actionSystem.ex.AnActionListener;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.*;
+import com.intellij.openapi.editor.EditorFactory;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.keymap.Keymap;
+import com.intellij.openapi.keymap.KeymapManager;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.DumbAwareRunnable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.StartupManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileAdapter;
+import com.intellij.openapi.vfs.VirtualFileEvent;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.wm.*;
+import com.intellij.util.xmlb.XmlSerializer;
+import com.jetbrains.python.edu.actions.StudyNextWindowAction;
+import com.jetbrains.python.edu.actions.StudyPrevWindowAction;
+import com.jetbrains.python.edu.actions.StudyShowHintAction;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.Lesson;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.ui.StudyCondition;
+import com.jetbrains.python.edu.ui.StudyToolWindowFactory;
+import org.jdom.Element;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of class which contains all the information
+ * about study in context of current project
+ */
+
+@State(
+ name = "StudySettings",
+ storages = {
+ @Storage(
+ id = "others",
+ file = "$PROJECT_CONFIG_DIR$/study_project.xml",
+ scheme = StorageScheme.DIRECTORY_BASED
+ )}
+)
+public class StudyTaskManager implements ProjectComponent, PersistentStateComponent<Element>, DumbAware {
+ public static final String COURSE_ELEMENT = "courseElement";
+ private static Map<String, StudyTaskManager> myTaskManagers = new HashMap<String, StudyTaskManager>();
+ private static Map<String, String> myDeletedShortcuts = new HashMap<String, String>();
+ private final Project myProject;
+ private Course myCourse;
+ private FileCreatedListener myListener;
+
+
+ public void setCourse(Course course) {
+ myCourse = course;
+ }
+
+ private StudyTaskManager(@NotNull final Project project) {
+ myTaskManagers.put(project.getBasePath(), this);
+ myProject = project;
+ }
+
+
+ @Nullable
+ public Course getCourse() {
+ return myCourse;
+ }
+
+ @Nullable
+ @Override
+ public Element getState() {
+ Element el = new Element("taskManager");
+ if (myCourse != null) {
+ Element courseElement = new Element(COURSE_ELEMENT);
+ XmlSerializer.serializeInto(myCourse, courseElement);
+ el.addContent(courseElement);
+ }
+ return el;
+ }
+
+ @Override
+ public void loadState(Element el) {
+ myCourse = XmlSerializer.deserialize(el.getChild(COURSE_ELEMENT), Course.class);
+ if (myCourse != null) {
+ myCourse.init(true);
+ }
+ }
+
+ @Override
+ public void projectOpened() {
+ ApplicationManager.getApplication().invokeLater(new DumbAwareRunnable() {
+ @Override
+ public void run() {
+ ApplicationManager.getApplication().runWriteAction(new DumbAwareRunnable() {
+ @Override
+ public void run() {
+ if (myCourse != null) {
+ StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() {
+ @Override
+ public void run() {
+ ToolWindowManager.getInstance(myProject).getToolWindow(ToolWindowId.PROJECT_VIEW).show(null);
+ FileEditor[] editors = FileEditorManager.getInstance(myProject).getSelectedEditors();
+ if (editors.length > 0) {
+ JComponent focusedComponent = editors[0].getPreferredFocusedComponent();
+ if (focusedComponent != null) {
+ IdeFocusManager.getInstance(myProject).requestFocus(focusedComponent, true);
+ }
+ }
+ }
+ });
+ UISettings.getInstance().HIDE_TOOL_STRIPES = false;
+ UISettings.getInstance().fireUISettingsChanged();
+ ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(myProject);
+ String toolWindowId = StudyToolWindowFactory.STUDY_TOOL_WINDOW;
+ //TODO:decide smth with tool window position
+ try {
+ Method method = toolWindowManager.getClass().getDeclaredMethod("registerToolWindow", String.class,
+ JComponent.class,
+ ToolWindowAnchor.class,
+ boolean.class, boolean.class, boolean.class);
+ method.setAccessible(true);
+ method.invoke(toolWindowManager, toolWindowId, null, ToolWindowAnchor.LEFT, true, true, true);
+ }
+ catch (Exception e) {
+ final ToolWindow toolWindow = toolWindowManager.getToolWindow(toolWindowId);
+ if (toolWindow == null)
+ toolWindowManager.registerToolWindow(toolWindowId, true, ToolWindowAnchor.RIGHT, myProject, true);
+ }
+
+ final ToolWindow studyToolWindow = toolWindowManager.getToolWindow(toolWindowId);
+ if (studyToolWindow != null) {
+ StudyUtils.updateStudyToolWindow(myProject);
+ studyToolWindow.show(null);
+ }
+ addShortcut(StudyNextWindowAction.SHORTCUT, StudyNextWindowAction.ACTION_ID);
+ addShortcut(StudyPrevWindowAction.SHORTCUT, StudyPrevWindowAction.ACTION_ID);
+ addShortcut(StudyShowHintAction.SHORTCUT, StudyShowHintAction.ACTION_ID);
+ addShortcut(StudyNextWindowAction.SHORTCUT2, StudyNextWindowAction.ACTION_ID);
+ }
+ }
+ });
+ }
+ });
+ }
+
+
+ private static void addShortcut(@NotNull final String shortcutString, @NotNull final String actionIdString) {
+ Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
+ Shortcut studyActionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcutString), null);
+ String[] actionsIds = keymap.getActionIds(studyActionShortcut);
+ for (String actionId : actionsIds) {
+ myDeletedShortcuts.put(actionId, shortcutString);
+ keymap.removeShortcut(actionId, studyActionShortcut);
+ }
+ keymap.addShortcut(actionIdString, studyActionShortcut);
+ }
+
+ @Override
+ public void projectClosed() {
+ StudyCondition.VALUE = false;
+ if (myCourse != null) {
+ ToolWindowManager.getInstance(myProject).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW).getContentManager()
+ .removeAllContents(false);
+ if (!myDeletedShortcuts.isEmpty()) {
+ for (Map.Entry<String, String> shortcut : myDeletedShortcuts.entrySet()) {
+ Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
+ Shortcut actionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcut.getValue()), null);
+ keymap.addShortcut(shortcut.getKey(), actionShortcut);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void initComponent() {
+ EditorFactory.getInstance().addEditorFactoryListener(new StudyEditorFactoryListener(), myProject);
+ ActionManager.getInstance().addAnActionListener(new AnActionListener() {
+ @Override
+ public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
+ AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
+ for (AnAction newAction : newGroupActions) {
+ if (newAction == action) {
+ myListener = new FileCreatedListener();
+ VirtualFileManager.getInstance().addVirtualFileListener(myListener);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
+ AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
+ for (AnAction newAction : newGroupActions) {
+ if (newAction == action) {
+ VirtualFileManager.getInstance().removeVirtualFileListener(myListener);
+ }
+ }
+ }
+
+ @Override
+ public void beforeEditorTyping(char c, DataContext dataContext) {
+
+ }
+ });
+ }
+
+ @Override
+ public void disposeComponent() {
+ }
+
+ @NotNull
+ @Override
+ public String getComponentName() {
+ return "StudyTaskManager";
+ }
+
+ public static StudyTaskManager getInstance(@NotNull final Project project) {
+ StudyTaskManager item = myTaskManagers.get(project.getBasePath());
+ return item != null ? item : new StudyTaskManager(project);
+ }
+
+
+ @Nullable
+ public TaskFile getTaskFile(@NotNull final VirtualFile file) {
+ if (myCourse == null) {
+ return null;
+ }
+ VirtualFile taskDir = file.getParent();
+ if (taskDir != null) {
+ String taskDirName = taskDir.getName();
+ if (taskDirName.contains(Task.TASK_DIR)) {
+ VirtualFile lessonDir = taskDir.getParent();
+ if (lessonDir != null) {
+ String lessonDirName = lessonDir.getName();
+ int lessonIndex = StudyUtils.getIndex(lessonDirName, Lesson.LESSON_DIR);
+ List<Lesson> lessons = myCourse.getLessons();
+ if (!StudyUtils.indexIsValid(lessonIndex, lessons)) {
+ return null;
+ }
+ Lesson lesson = lessons.get(lessonIndex);
+ int taskIndex = StudyUtils.getIndex(taskDirName, Task.TASK_DIR);
+ List<Task> tasks = lesson.getTaskList();
+ if (!StudyUtils.indexIsValid(taskIndex, tasks)) {
+ return null;
+ }
+ Task task = tasks.get(taskIndex);
+ return task.getFile(file.getName());
+ }
+ }
+ }
+ return null;
+ }
+
+ class FileCreatedListener extends VirtualFileAdapter {
+ @Override
+ public void fileCreated(@NotNull VirtualFileEvent event) {
+ VirtualFile createdFile = event.getFile();
+ VirtualFile taskDir = createdFile.getParent();
+ String taskLogicalName = Task.TASK_DIR;
+ if (taskDir != null && taskDir.getName().contains(taskLogicalName)) {
+ int taskIndex = StudyUtils.getIndex(taskDir.getName(), taskLogicalName);
+ VirtualFile lessonDir = taskDir.getParent();
+ String lessonLogicalName = Lesson.LESSON_DIR;
+ if (lessonDir != null && lessonDir.getName().contains(lessonLogicalName)) {
+ int lessonIndex = StudyUtils.getIndex(lessonDir.getName(), lessonLogicalName);
+ if (myCourse != null) {
+ List<Lesson> lessons = myCourse.getLessons();
+ if (StudyUtils.indexIsValid(lessonIndex, lessons)) {
+ Lesson lesson = lessons.get(lessonIndex);
+ List<Task> tasks = lesson.getTaskList();
+ if (StudyUtils.indexIsValid(taskIndex, tasks)) {
+ Task task = tasks.get(taskIndex);
+ TaskFile taskFile = new TaskFile();
+ taskFile.init(task, false);
+ taskFile.setUserCreated(true);
+ task.getTaskFiles().put(createdFile.getName(), taskFile);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyUtils.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyUtils.java
new file mode 100644
index 000000000000..d3ac1dadf98e
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyUtils.java
@@ -0,0 +1,151 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.ide.SaveAndSyncHandlerImpl;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.wm.ToolWindowManager;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.edu.ui.StudyToolWindowFactory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.*;
+import java.util.Collection;
+
+public class StudyUtils {
+ private static final Logger LOG = Logger.getInstance(StudyUtils.class.getName());
+ public static void closeSilently(Closeable stream) {
+ if (stream != null) {
+ try {
+ stream.close();
+ }
+ catch (IOException e) {
+ // close silently
+ }
+ }
+ }
+
+ public static boolean isZip(String fileName) {
+ return fileName.contains(".zip");
+ }
+
+ public static <T> T getFirst(Iterable<T> container) {
+ return container.iterator().next();
+ }
+
+ public static boolean indexIsValid(int index, Collection collection) {
+ int size = collection.size();
+ return index >= 0 && index < size;
+ }
+
+ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+ @Nullable
+ public static String getFileText(String parentDir, String fileName, boolean wrapHTML) {
+
+ File inputFile = parentDir !=null ? new File(parentDir, fileName) : new File(fileName);
+ if (!inputFile.exists()) return null;
+ StringBuilder taskText = new StringBuilder();
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile)));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ taskText.append(line).append("\n");
+ if (wrapHTML) {
+ taskText.append("<br>");
+ }
+ }
+ return wrapHTML ? UIUtil.toHtml(taskText.toString()) : taskText.toString();
+ }
+ catch (IOException e) {
+ LOG.error("Failed to get file text from file " + fileName, e);
+ }
+ finally {
+ closeSilently(reader);
+ }
+ return null;
+ }
+
+ public static void updateAction(AnActionEvent e) {
+ Presentation presentation = e.getPresentation();
+ presentation.setEnabled(false);
+ Project project = e.getProject();
+ if (project != null) {
+ FileEditor[] editors = FileEditorManager.getInstance(project).getAllEditors();
+ for (FileEditor editor : editors) {
+ if (editor instanceof StudyEditor) {
+ presentation.setEnabled(true);
+ }
+ }
+ }
+ }
+
+ public static void updateStudyToolWindow(Project project) {
+ ToolWindowManager.getInstance(project).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW).getContentManager().removeAllContents(false);
+ StudyToolWindowFactory factory = new StudyToolWindowFactory();
+ factory.createToolWindowContent(project, ToolWindowManager.getInstance(project).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW));
+ }
+
+ public static void synchronize() {
+ FileDocumentManager.getInstance().saveAllDocuments();
+ SaveAndSyncHandlerImpl.refreshOpenFiles();
+ VirtualFileManager.getInstance().refreshWithoutFileWatcher(true);
+ }
+
+ /**
+ * Gets number index in directory names like "task1", "lesson2"
+ *
+ * @param fullName full name of directory
+ * @param logicalName part of name without index
+ * @return index of object
+ */
+ public static int getIndex(@NotNull final String fullName, @NotNull final String logicalName) {
+ if (!fullName.contains(logicalName)) {
+ throw new IllegalArgumentException();
+ }
+ return Integer.parseInt(fullName.substring(logicalName.length())) - 1;
+ }
+
+ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+ public static VirtualFile flushWindows(Document document, TaskFile taskFile, VirtualFile file) {
+ VirtualFile taskDir = file.getParent();
+ VirtualFile fileWindows = null;
+ if (taskDir != null) {
+ String name = file.getNameWithoutExtension() + "_windows";
+ PrintWriter printWriter = null;
+ try {
+
+ fileWindows = taskDir.createChildData(taskFile, name);
+ printWriter = new PrintWriter(new FileOutputStream(fileWindows.getPath()));
+ for (TaskWindow taskWindow : taskFile.getTaskWindows()) {
+ if (!taskWindow.isValid(document)) {
+ continue;
+ }
+ int start = taskWindow.getRealStartOffset(document);
+ String windowDescription = document.getText(new TextRange(start, start + taskWindow.getLength()));
+ printWriter.println("#study_plugin_window = " + windowDescription);
+ }
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ finally {
+ closeSilently(printWriter);
+ synchronize();
+ }
+ }
+ return fileWindows;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyCheckAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyCheckAction.java
new file mode 100644
index 000000000000..f8e10c9c4521
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyCheckAction.java
@@ -0,0 +1,340 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.ide.projectView.ProjectView;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.CommandProcessor;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.BalloonBuilder;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.JBColor;
+import com.jetbrains.python.edu.StudyDocumentListener;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.*;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.sdk.PythonSdkType;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import java.util.List;
+
+public class StudyCheckAction extends DumbAwareAction {
+
+ private static final Logger LOG = Logger.getInstance(StudyCheckAction.class.getName());
+ public static final String PYTHONPATH = "PYTHONPATH";
+
+ static class StudyTestRunner {
+ public static final String TEST_OK = "#study_plugin test OK";
+ private static final String TEST_FAILED = "#study_plugin FAILED + ";
+ private final Task myTask;
+ private final VirtualFile myTaskDir;
+
+ StudyTestRunner(Task task, VirtualFile taskDir) {
+ myTask = task;
+ myTaskDir = taskDir;
+ }
+
+ Process launchTests(Project project, String executablePath) throws ExecutionException {
+ Sdk sdk = PythonSdkType.findPythonSdk(ModuleManager.getInstance(project).getModules()[0]);
+ File testRunner = new File(myTaskDir.getPath(), myTask.getTestFile());
+ GeneralCommandLine commandLine = new GeneralCommandLine();
+ commandLine.setWorkDirectory(myTaskDir.getPath());
+ final Map<String, String> env = commandLine.getEnvironment();
+ final VirtualFile courseDir = project.getBaseDir();
+ if (courseDir != null)
+ env.put(PYTHONPATH, courseDir.getPath());
+ if (sdk != null) {
+ String pythonPath = sdk.getHomePath();
+ if (pythonPath != null) {
+ commandLine.setExePath(pythonPath);
+ commandLine.addParameter(testRunner.getPath());
+ final Course course = StudyTaskManager.getInstance(project).getCourse();
+ assert course != null;
+ commandLine.addParameter(new File(course.getResourcePath()).getParent());
+ commandLine.addParameter(FileUtil.toSystemDependentName(executablePath));
+ return commandLine.createProcess();
+ }
+ }
+ return null;
+ }
+
+
+ String getPassedTests(Process p) {
+ InputStream testOutput = p.getInputStream();
+ BufferedReader testOutputReader = new BufferedReader(new InputStreamReader(testOutput));
+ String line;
+ try {
+ while ((line = testOutputReader.readLine()) != null) {
+ if (line.contains(TEST_FAILED)) {
+ return line.substring(TEST_FAILED.length(), line.length());
+ }
+ }
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ finally {
+ StudyUtils.closeSilently(testOutputReader);
+ }
+ return TEST_OK;
+ }
+ }
+
+ public void check(@NotNull final Project project) {
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() {
+ @Override
+ public void run() {
+ final Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+ if (selectedEditor != null) {
+ final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+ final VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+ if (openedFile != null) {
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ final TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+ List<VirtualFile> filesToDelete = new ArrayList<VirtualFile>();
+ if (selectedTaskFile != null) {
+ final VirtualFile taskDir = openedFile.getParent();
+ Task currentTask = selectedTaskFile.getTask();
+ StudyStatus oldStatus = currentTask.getStatus();
+ Map<String, TaskFile> taskFiles = selectedTaskFile.getTask().getTaskFiles();
+ for (Map.Entry<String, TaskFile> entry : taskFiles.entrySet()) {
+ String name = entry.getKey();
+ TaskFile taskFile = entry.getValue();
+ VirtualFile virtualFile = taskDir.findChild(name);
+ if (virtualFile == null) {
+ continue;
+ }
+ VirtualFile windowFile = StudyUtils.flushWindows(FileDocumentManager.getInstance().getDocument(virtualFile), taskFile, virtualFile);
+ filesToDelete.add(windowFile);
+ FileDocumentManager.getInstance().saveAllDocuments();
+ }
+
+ StudyRunAction runAction = (StudyRunAction)ActionManager.getInstance().getAction(StudyRunAction.ACTION_ID);
+ if (runAction != null && currentTask.getTaskFiles().size() == 1) {
+ runAction.run(project);
+ }
+ final StudyTestRunner testRunner = new StudyTestRunner(currentTask, taskDir);
+ Process testProcess = null;
+ try {
+ testProcess = testRunner.launchTests(project, openedFile.getPath());
+ }
+ catch (ExecutionException e) {
+ LOG.error(e);
+ }
+ if (testProcess != null) {
+ String failedMessage = testRunner.getPassedTests(testProcess);
+ if (failedMessage.equals(StudyTestRunner.TEST_OK)) {
+ currentTask.setStatus(StudyStatus.Solved, oldStatus);
+ StudyUtils.updateStudyToolWindow(project);
+ selectedTaskFile.drawAllWindows(selectedEditor);
+ ProjectView.getInstance(project).refresh();
+ for (VirtualFile file:filesToDelete) {
+ try {
+ file.delete(this);
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ }
+ createTestResultPopUp("Congratulations!", JBColor.GREEN, project);
+ return;
+ }
+ for (Map.Entry<String, TaskFile> entry : taskFiles.entrySet()) {
+ String name = entry.getKey();
+ TaskFile taskFile = entry.getValue();
+ TaskFile answerTaskFile = new TaskFile();
+ VirtualFile virtualFile = taskDir.findChild(name);
+ if (virtualFile == null) {
+ continue;
+ }
+ VirtualFile answerFile = getCopyWithAnswers(taskDir, virtualFile, taskFile, answerTaskFile);
+ for (TaskWindow taskWindow : answerTaskFile.getTaskWindows()) {
+ Document document = FileDocumentManager.getInstance().getDocument(virtualFile);
+ if (document == null) {
+ continue;
+ }
+ if (!taskWindow.isValid(document)) {
+ continue;
+ }
+ check(project, taskWindow, answerFile, answerTaskFile, taskFile, document, testRunner, virtualFile);
+ }
+ FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(virtualFile);
+ Editor editor = null;
+ if (fileEditor instanceof StudyEditor) {
+ StudyEditor studyEditor = (StudyEditor) fileEditor;
+ editor = studyEditor.getEditor();
+ }
+
+ if (editor != null) {
+ taskFile.drawAllWindows(editor);
+ StudyUtils.synchronize();
+ }
+ try {
+ answerFile.delete(this);
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ }
+ for (VirtualFile file:filesToDelete) {
+ try {
+ file.delete(this);
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ }
+ currentTask.setStatus(StudyStatus.Failed, oldStatus);
+ StudyUtils.updateStudyToolWindow(project);
+ createTestResultPopUp(failedMessage, JBColor.RED, project);
+ }
+ }
+ }
+ }
+
+ }
+ });
+ }
+ });
+ }
+
+ private void check(Project project,
+ TaskWindow taskWindow,
+ VirtualFile answerFile,
+ TaskFile answerTaskFile,
+ TaskFile usersTaskFile,
+ Document usersDocument,
+ StudyTestRunner testRunner,
+ VirtualFile openedFile) {
+
+ try {
+ VirtualFile windowCopy = answerFile.copy(this, answerFile.getParent(), answerFile.getNameWithoutExtension() + "_window" + taskWindow.getIndex() + ".py");
+ final FileDocumentManager documentManager = FileDocumentManager.getInstance();
+ final Document windowDocument = documentManager.getDocument(windowCopy);
+ if (windowDocument != null) {
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ Course course = taskManager.getCourse();
+ Task task = usersTaskFile.getTask();
+ int taskNum = task.getIndex() + 1;
+ int lessonNum = task.getLesson().getIndex() + 1;
+ assert course != null;
+ String pathToResource = FileUtil.join(new File(course.getResourcePath()).getParent(), Lesson.LESSON_DIR + lessonNum, Task.TASK_DIR + taskNum);
+ File resourceFile = new File(pathToResource, windowCopy.getName());
+ FileUtil.copy(new File(pathToResource, openedFile.getName()), resourceFile);
+ TaskFile windowTaskFile = new TaskFile();
+ TaskFile.copy(answerTaskFile, windowTaskFile);
+ StudyDocumentListener listener = new StudyDocumentListener(windowTaskFile);
+ windowDocument.addDocumentListener(listener);
+ int start = taskWindow.getRealStartOffset(windowDocument);
+ int end = start + taskWindow.getLength();
+ TaskWindow userTaskWindow = usersTaskFile.getTaskWindows().get(taskWindow.getIndex());
+ int userStart = userTaskWindow.getRealStartOffset(usersDocument);
+ int userEnd = userStart + userTaskWindow.getLength();
+ String text = usersDocument.getText(new TextRange(userStart, userEnd));
+ windowDocument.replaceString(start, end, text);
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ documentManager.saveDocument(windowDocument);
+ }
+ });
+ VirtualFile fileWindows = StudyUtils.flushWindows(windowDocument, windowTaskFile, windowCopy);
+ Process smartTestProcess = testRunner.launchTests(project, windowCopy.getPath());
+ boolean res = testRunner.getPassedTests(smartTestProcess).equals(StudyTestRunner.TEST_OK);
+ userTaskWindow.setStatus(res ? StudyStatus.Solved : StudyStatus.Failed, StudyStatus.Unchecked);
+ windowCopy.delete(this);
+ fileWindows.delete(this);
+ if (!resourceFile.delete()) {
+ LOG.error("failed to delete", resourceFile.getPath());
+ }
+ }
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ catch (ExecutionException e) {
+ LOG.error(e);
+ }
+ }
+
+
+ private VirtualFile getCopyWithAnswers(final VirtualFile taskDir,
+ final VirtualFile file,
+ final TaskFile source,
+ TaskFile target) {
+ VirtualFile copy = null;
+ try {
+
+ copy = file.copy(this, taskDir, file.getNameWithoutExtension() +"_answers.py");
+ final FileDocumentManager documentManager = FileDocumentManager.getInstance();
+ final Document document = documentManager.getDocument(copy);
+ if (document != null) {
+ TaskFile.copy(source, target);
+ StudyDocumentListener listener = new StudyDocumentListener(target);
+ document.addDocumentListener(listener);
+ for (TaskWindow taskWindow : target.getTaskWindows()) {
+ if (!taskWindow.isValid(document)) {
+ continue;
+ }
+ final int start = taskWindow.getRealStartOffset(document);
+ final int end = start + taskWindow.getLength();
+ final String text = taskWindow.getPossibleAnswer();
+ document.replaceString(start, end, text);
+ }
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ documentManager.saveDocument(document);
+ }
+ });
+ }
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+
+
+ return copy;
+ }
+
+ private static void createTestResultPopUp(final String text, Color color, @NotNull final Project project) {
+ BalloonBuilder balloonBuilder =
+ JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(text, null, color, null);
+ Balloon balloon = balloonBuilder.createBalloon();
+ StudyEditor studyEditor = StudyEditor.getSelectedStudyEditor(project);
+ assert studyEditor != null;
+ JButton checkButton = studyEditor.getCheckButton();
+ balloon.showInCenterOf(checkButton);
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ Project project = e.getProject();
+ if (project != null) {
+ check(project);
+ }
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyEditInputAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyEditInputAction.java
new file mode 100644
index 000000000000..5b9a6fef23ac
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyEditInputAction.java
@@ -0,0 +1,213 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.ui.UISettings;
+import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopup;
+import com.intellij.openapi.ui.popup.JBPopupAdapter;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.LightweightWindowEvent;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.wm.IdeFocusManager;
+import com.intellij.ui.tabs.TabInfo;
+import com.intellij.ui.tabs.TabsListener;
+import com.intellij.ui.tabs.impl.JBEditorTabs;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.UserTest;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.edu.ui.StudyTestContentPanel;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StudyEditInputAction extends DumbAwareAction {
+
+ public static final String TEST_TAB_NAME = "test";
+ public static final String USER_TEST_INPUT = "input";
+ public static final String USER_TEST_OUTPUT = "output";
+ private static final Logger LOG = Logger.getInstance(StudyEditInputAction.class.getName());
+ private JBEditorTabs tabbedPane;
+ private Map<TabInfo, UserTest> myEditableTabs = new HashMap<TabInfo, UserTest>();
+
+ public void showInput(final Project project) {
+ final Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+ if (selectedEditor != null) {
+ FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+ final VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+ StudyTaskManager studyTaskManager = StudyTaskManager.getInstance(project);
+ assert openedFile != null;
+ TaskFile taskFile = studyTaskManager.getTaskFile(openedFile);
+ assert taskFile != null;
+ final Task currentTask = taskFile.getTask();
+ tabbedPane = new JBEditorTabs(project, ActionManager.getInstance(), IdeFocusManager.findInstance(), project);
+ tabbedPane.addListener(new TabsListener.Adapter() {
+ @Override
+ public void selectionChanged(TabInfo oldSelection, TabInfo newSelection) {
+ if (newSelection.getIcon() != null) {
+ int tabCount = tabbedPane.getTabCount();
+ VirtualFile taskDir = openedFile.getParent();
+ VirtualFile testsDir = taskDir.findChild(Task.USER_TESTS);
+ assert testsDir != null;
+ UserTest userTest = createUserTest(testsDir, currentTask);
+ userTest.setEditable(true);
+ StudyTestContentPanel testContentPanel = new StudyTestContentPanel(userTest);
+ TabInfo testTab = addTestTab(tabbedPane.getTabCount(), testContentPanel, currentTask, true);
+ myEditableTabs.put(testTab, userTest);
+ tabbedPane.addTabSilently(testTab, tabCount - 1);
+ tabbedPane.select(testTab, true);
+ }
+ }
+ });
+ List<UserTest> userTests = currentTask.getUserTests();
+ int i = 1;
+ for (UserTest userTest : userTests) {
+ String inputFileText = StudyUtils.getFileText(null, userTest.getInput(), false);
+ String outputFileText = StudyUtils.getFileText(null, userTest.getOutput(), false);
+ StudyTestContentPanel myContentPanel = new StudyTestContentPanel(userTest);
+ myContentPanel.addInputContent(inputFileText);
+ myContentPanel.addOutputContent(outputFileText);
+ TabInfo testTab = addTestTab(i, myContentPanel, currentTask, userTest.isEditable());
+ tabbedPane.addTabSilently(testTab, i - 1);
+ if (userTest.isEditable()) {
+ myEditableTabs.put(testTab, userTest);
+ }
+ i++;
+ }
+ TabInfo plusTab = new TabInfo(new JPanel());
+ plusTab.setIcon(StudyIcons.Add);
+ tabbedPane.addTabSilently(plusTab, tabbedPane.getTabCount());
+ final JBPopup hint =
+ JBPopupFactory.getInstance().createComponentPopupBuilder(tabbedPane.getComponent(), tabbedPane.getComponent())
+ .setResizable(true)
+ .setMovable(true)
+ .setRequestFocus(true)
+ .createPopup();
+ StudyEditor selectedStudyEditor = StudyEditor.getSelectedStudyEditor(project);
+ assert selectedStudyEditor != null;
+ hint.showInCenterOf(selectedStudyEditor.getComponent());
+ hint.addListener(new HintClosedListener(currentTask));
+ }
+ }
+
+
+ private static void flushBuffer(@NotNull final StringBuilder buffer, @NotNull final File file) {
+ PrintWriter printWriter = null;
+ try {
+ printWriter = new PrintWriter(new FileOutputStream(file));
+ printWriter.print(buffer.toString());
+ }
+ catch (FileNotFoundException e) {
+ LOG.error(e);
+ }
+ finally {
+ StudyUtils.closeSilently(printWriter);
+ }
+ StudyUtils.synchronize();
+ }
+
+ private static UserTest createUserTest(@NotNull final VirtualFile testsDir, @NotNull final Task currentTask) {
+ UserTest userTest = new UserTest();
+ List<UserTest> userTests = currentTask.getUserTests();
+ int testNum = userTests.size() + 1;
+ String inputName = USER_TEST_INPUT + testNum;
+ File inputFile = new File(testsDir.getPath(), inputName);
+ String outputName = USER_TEST_OUTPUT + testNum;
+ File outputFile = new File(testsDir.getPath(), outputName);
+ userTest.setInput(inputFile.getPath());
+ userTest.setOutput(outputFile.getPath());
+ userTests.add(userTest);
+ return userTest;
+ }
+
+ private TabInfo addTestTab(int nameIndex, final StudyTestContentPanel contentPanel, @NotNull final Task currentTask, boolean toBeClosable) {
+ TabInfo testTab = toBeClosable ? createClosableTab(contentPanel, currentTask) : new TabInfo(contentPanel);
+ return testTab.setText(TEST_TAB_NAME + String.valueOf(nameIndex));
+ }
+
+ private TabInfo createClosableTab(StudyTestContentPanel contentPanel, Task currentTask) {
+ TabInfo closableTab = new TabInfo(contentPanel);
+ final DefaultActionGroup tabActions = new DefaultActionGroup();
+ tabActions.add(new CloseTab(closableTab, currentTask));
+ closableTab.setTabLabelActions(tabActions, ActionPlaces.EDITOR_TAB);
+ return closableTab;
+ }
+
+ public void actionPerformed(AnActionEvent e) {
+ showInput(e.getProject());
+ }
+
+ private class HintClosedListener extends JBPopupAdapter {
+ private final Task myTask;
+ private HintClosedListener(@NotNull final Task task) {
+ myTask = task;
+ }
+
+ @Override
+ public void onClosed(LightweightWindowEvent event) {
+ for (final UserTest userTest : myTask.getUserTests()) {
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ if (userTest.isEditable()) {
+ File inputFile = new File(userTest.getInput());
+ File outputFile = new File(userTest.getOutput());
+ flushBuffer(userTest.getInputBuffer(), inputFile);
+ flushBuffer(userTest.getOutputBuffer(), outputFile);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ private class CloseTab extends AnAction implements DumbAware {
+
+ private final TabInfo myTabInfo;
+ private final Task myTask;
+
+ public CloseTab(final TabInfo info, @NotNull final Task task) {
+ myTabInfo = info;
+ myTask = task;
+ }
+
+ @Override
+ public void update(final AnActionEvent e) {
+ e.getPresentation().setIcon(tabbedPane.isEditorTabs() ? AllIcons.Actions.CloseNew : AllIcons.Actions.Close);
+ e.getPresentation().setHoveredIcon(tabbedPane.isEditorTabs() ? AllIcons.Actions.CloseNewHovered : AllIcons.Actions.CloseHovered);
+ e.getPresentation().setVisible(UISettings.getInstance().SHOW_CLOSE_BUTTON);
+ e.getPresentation().setText("Delete test");
+ }
+
+ @Override
+ public void actionPerformed(final AnActionEvent e) {
+ tabbedPane.removeTab(myTabInfo);
+ UserTest userTest = myEditableTabs.get(myTabInfo);
+ File testInputFile = new File(userTest.getInput());
+ File testOutputFile = new File(userTest.getOutput());
+ if (testInputFile.delete() && testOutputFile.delete()) {
+ StudyUtils.synchronize();
+ } else {
+ LOG.error("failed to delete user tests");
+ }
+ myTask.getUserTests().remove(userTest);
+ }
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextStudyTaskAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextStudyTaskAction.java
new file mode 100644
index 000000000000..81818a95c044
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextStudyTaskAction.java
@@ -0,0 +1,24 @@
+package com.jetbrains.python.edu.actions;
+
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.edu.course.Task;
+
+import javax.swing.*;
+
+public class StudyNextStudyTaskAction extends StudyTaskNavigationAction {
+
+ @Override
+ protected JButton getButton(StudyEditor selectedStudyEditor) {
+ return selectedStudyEditor.getNextTaskButton();
+ }
+
+ @Override
+ protected String getNavigationFinishedMessage() {
+ return "It's the last task";
+ }
+
+ @Override
+ protected Task getTargetTask(Task sourceTask) {
+ return sourceTask.next();
+ }
+} \ No newline at end of file
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextWindowAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextWindowAction.java
new file mode 100644
index 000000000000..595aeeff42e3
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextWindowAction.java
@@ -0,0 +1,32 @@
+package com.jetbrains.python.edu.actions;
+
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.TaskWindow;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * move caret to next task window
+ */
+public class StudyNextWindowAction extends StudyWindowNavigationAction {
+ public static final String ACTION_ID = "NextWindow";
+ public static final String SHORTCUT = "ctrl pressed PERIOD";
+ public static final String SHORTCUT2 = "ctrl pressed ENTER";
+
+ public StudyNextWindowAction() {
+ super("NextWindowAction", "Select next window", StudyIcons.Next);
+ }
+
+ @Override
+ protected TaskWindow getNextTaskWindow(@NotNull final TaskWindow window) {
+ int index = window.getIndex();
+ List<TaskWindow> windows = window.getTaskFile().getTaskWindows();
+ if (StudyUtils.indexIsValid(index, windows)) {
+ int newIndex = index + 1;
+ return newIndex == windows.size() ? windows.get(0) : windows.get(newIndex);
+ }
+ return null;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPrevWindowAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPrevWindowAction.java
new file mode 100644
index 000000000000..347456189a00
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPrevWindowAction.java
@@ -0,0 +1,34 @@
+package com.jetbrains.python.edu.actions;
+
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.TaskWindow;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * author: liana
+ * data: 6/30/14.
+ */
+public class StudyPrevWindowAction extends StudyWindowNavigationAction {
+ public static final String ACTION_ID = "PrevWindowAction";
+ public static final String SHORTCUT = "ctrl pressed COMMA";
+
+ public StudyPrevWindowAction() {
+ super("PrevWindowAction", "Select previous window", StudyIcons.Prev);
+ }
+
+
+ @Nullable
+ @Override
+ protected TaskWindow getNextTaskWindow(@NotNull final TaskWindow window) {
+ int prevIndex = window.getIndex() - 1;
+ List<TaskWindow> windows = window.getTaskFile().getTaskWindows();
+ if (StudyUtils.indexIsValid(prevIndex, windows)) {
+ return windows.get(prevIndex);
+ }
+ return null;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPreviousStudyTaskAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPreviousStudyTaskAction.java
new file mode 100644
index 000000000000..bc26c28cfabd
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPreviousStudyTaskAction.java
@@ -0,0 +1,25 @@
+package com.jetbrains.python.edu.actions;
+
+
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.edu.course.Task;
+
+import javax.swing.*;
+
+public class StudyPreviousStudyTaskAction extends StudyTaskNavigationAction {
+
+ @Override
+ protected JButton getButton(StudyEditor selectedStudyEditor) {
+ return selectedStudyEditor.getPrevTaskButton();
+ }
+
+ @Override
+ protected String getNavigationFinishedMessage() {
+ return "It's already the first task";
+ }
+
+ @Override
+ protected Task getTargetTask(Task sourceTask) {
+ return sourceTask.prev();
+ }
+} \ No newline at end of file
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskAction.java
new file mode 100644
index 000000000000..f8abb0b63365
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskAction.java
@@ -0,0 +1,122 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.ide.projectView.ProjectView;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.CommandProcessor;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.BalloonBuilder;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.wm.IdeFocusManager;
+import com.jetbrains.python.edu.StudyDocumentListener;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.*;
+import com.jetbrains.python.edu.editor.StudyEditor;
+
+import java.io.*;
+
+public class StudyRefreshTaskAction extends DumbAwareAction {
+ private static final Logger LOG = Logger.getInstance(StudyRefreshTaskAction.class.getName());
+
+ public void refresh(final Project project) {
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+ @Override
+ public void run() {
+ final Editor editor = StudyEditor.getSelectedEditor(project);
+ assert editor != null;
+ final Document document = editor.getDocument();
+ StudyDocumentListener listener = StudyEditor.getListener(document);
+ if (listener != null) {
+ document.removeDocumentListener(listener);
+ }
+ final int lineCount = document.getLineCount();
+ if (lineCount != 0) {
+ CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() {
+ @Override
+ public void run() {
+ document.deleteString(0, document.getLineEndOffset(lineCount - 1));
+ }
+ });
+ }
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ Course course = taskManager.getCourse();
+ assert course != null;
+ File resourceFile = new File(course.getResourcePath());
+ File resourceRoot = resourceFile.getParentFile();
+ FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+ VirtualFile openedFile = fileDocumentManager.getFile(document);
+ assert openedFile != null;
+ final TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+ assert selectedTaskFile != null;
+ Task currentTask = selectedTaskFile.getTask();
+ String lessonDir = Lesson.LESSON_DIR + String.valueOf(currentTask.getLesson().getIndex() + 1);
+ String taskDir = Task.TASK_DIR + String.valueOf(currentTask.getIndex() + 1);
+ File pattern = new File(new File(new File(resourceRoot, lessonDir), taskDir), openedFile.getName());
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new InputStreamReader(new FileInputStream(pattern)));
+ String line;
+ StringBuilder patternText = new StringBuilder();
+ while ((line = reader.readLine()) != null) {
+ patternText.append(line);
+ patternText.append("\n");
+ }
+ int patternLength = patternText.length();
+ if (patternText.charAt(patternLength - 1) == '\n') {
+ patternText.delete(patternLength - 1, patternLength);
+ }
+ document.setText(patternText);
+ StudyStatus oldStatus = currentTask.getStatus();
+ LessonInfo lessonInfo = currentTask.getLesson().getLessonInfo();
+ lessonInfo.update(oldStatus, -1);
+ lessonInfo.update(StudyStatus.Unchecked, +1);
+ StudyUtils.updateStudyToolWindow(project);
+ for (TaskWindow taskWindow : selectedTaskFile.getTaskWindows()) {
+ taskWindow.reset();
+ }
+ ProjectView.getInstance(project).refresh();
+ if (listener != null) {
+ document.addDocumentListener(listener);
+ }
+ selectedTaskFile.drawAllWindows(editor);
+ IdeFocusManager.getInstance(project).requestFocus(editor.getContentComponent(), true);
+ selectedTaskFile.navigateToFirstTaskWindow(editor);
+ BalloonBuilder balloonBuilder =
+ JBPopupFactory.getInstance().createHtmlTextBalloonBuilder("You can now start again", MessageType.INFO, null);
+ Balloon balloon = balloonBuilder.createBalloon();
+ StudyEditor selectedStudyEditor = StudyEditor.getSelectedStudyEditor(project);
+ assert selectedStudyEditor != null;
+ balloon.showInCenterOf(selectedStudyEditor.getRefreshButton());
+ }
+ catch (FileNotFoundException e1) {
+ LOG.error(e1);
+ }
+ catch (IOException e1) {
+ LOG.error(e1);
+ }
+ finally {
+ StudyUtils.closeSilently(reader);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ public void actionPerformed(AnActionEvent e) {
+ refresh(e.getProject());
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRunAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRunAction.java
new file mode 100644
index 000000000000..71e95defdedc
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRunAction.java
@@ -0,0 +1,89 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.RunContentExecutor;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.OSProcessHandler;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.sdk.PythonSdkType;
+import com.jetbrains.python.edu.StudyResourceManger;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.editor.StudyEditor;
+
+import java.io.File;
+
+public class StudyRunAction extends DumbAwareAction {
+ private static final Logger LOG = Logger.getInstance(StudyRunAction.class.getName());
+ public static final String ACTION_ID = "StudyRunAction";
+
+ public void run(Project project) {
+ Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+ FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+ assert selectedEditor != null;
+ VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ if (openedFile != null && openedFile.getCanonicalPath() != null) {
+ String filePath = openedFile.getCanonicalPath();
+ GeneralCommandLine cmd = new GeneralCommandLine();
+ cmd.setWorkDirectory(openedFile.getParent().getCanonicalPath());
+ Sdk sdk = PythonSdkType.findPythonSdk(ModuleManager.getInstance(project).getModules()[0]);
+ if (sdk != null) {
+ String pythonPath = sdk.getHomePath();
+ if (pythonPath != null) {
+ cmd.setExePath(pythonPath);
+ TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+ assert selectedTaskFile != null;
+ Task currentTask = selectedTaskFile.getTask();
+ if (!currentTask.getUserTests().isEmpty()) {
+ cmd.addParameter(new File(project.getBaseDir().getPath(), StudyResourceManger.USER_TESTER).getPath());
+ cmd.addParameter(pythonPath);
+ cmd.addParameter(filePath);
+ Process p;
+ try {
+ p = cmd.createProcess();
+ }
+ catch (ExecutionException e) {
+ LOG.error(e);
+ return;
+ }
+ ProcessHandler handler = new OSProcessHandler(p);
+
+ RunContentExecutor executor = new RunContentExecutor(project, handler);
+ Disposer.register(project, executor);
+ executor.run();
+ return;
+ }
+ try {
+ cmd.addParameter(filePath);
+ Process p = cmd.createProcess();
+ ProcessHandler handler = new OSProcessHandler(p);
+
+ RunContentExecutor executor = new RunContentExecutor(project, handler);
+ Disposer.register(project, executor);
+ executor.run();
+ }
+
+ catch (ExecutionException e) {
+ LOG.error(e);
+ }
+ }
+ }
+ }
+ }
+
+ public void actionPerformed(AnActionEvent e) {
+ run(e.getProject());
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyShowHintAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyShowHintAction.java
new file mode 100644
index 000000000000..1efa90889449
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyShowHintAction.java
@@ -0,0 +1,95 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.codeInsight.documentation.DocumentationComponent;
+import com.intellij.codeInsight.documentation.DocumentationManager;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopup;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import icons.StudyIcons;
+
+import java.io.File;
+
+public class StudyShowHintAction extends DumbAwareAction {
+ public static final String ACTION_ID = "ShowHintAction";
+ public static final String SHORTCUT = "ctrl pressed 7";
+
+ public StudyShowHintAction() {
+ super("Show hint", "Show hint", StudyIcons.ShowHint);
+ }
+
+ public void actionPerformed(AnActionEvent e) {
+ Project project = e.getProject();
+ if (project != null) {
+ DocumentationManager documentationManager = DocumentationManager.getInstance(project);
+ DocumentationComponent component = new DocumentationComponent(documentationManager);
+ Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+ FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+ assert selectedEditor != null;
+ VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+ if (openedFile != null) {
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(e.getProject());
+ TaskFile taskFile = taskManager.getTaskFile(openedFile);
+ if (taskFile != null) {
+ PsiFile file = PsiManager.getInstance(project).findFile(openedFile);
+ if (file != null) {
+ LogicalPosition pos = selectedEditor.getCaretModel().getLogicalPosition();
+ TaskWindow taskWindow = taskFile.getTaskWindow(selectedEditor.getDocument(), pos);
+ if (taskWindow != null) {
+ String hint = taskWindow.getHint();
+ if (hint == null) {
+ return;
+ }
+ Course course = taskManager.getCourse();
+ if (course != null) {
+ File resourceFile = new File(course.getResourcePath());
+ File resourceRoot = resourceFile.getParentFile();
+ if (resourceRoot != null && resourceRoot.exists()) {
+ File hintsDir = new File(resourceRoot, Course.HINTS_DIR);
+ if (hintsDir.exists()) {
+ String hintText = StudyUtils.getFileText(hintsDir.getAbsolutePath(), hint, true);
+ if (hintText != null) {
+ int offset = selectedEditor.getDocument().getLineStartOffset(pos.line) + pos.column;
+ PsiElement element = file.findElementAt(offset);
+ if (element != null) {
+ component.setData(element, hintText, true, null);
+ final JBPopup popup =
+ JBPopupFactory.getInstance().createComponentPopupBuilder(component, component)
+ .setDimensionServiceKey(project, DocumentationManager.JAVADOC_LOCATION_AND_SIZE, false)
+ .setResizable(true)
+ .setMovable(true)
+ .setRequestFocus(true)
+ .createPopup();
+ component.setHint(popup);
+ popup.showInBestPositionFor(selectedEditor);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ StudyUtils.updateAction(e);
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java
new file mode 100644
index 000000000000..b781e7da8849
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java
@@ -0,0 +1,97 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.BalloonBuilder;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.Lesson;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.editor.StudyEditor;
+
+import javax.swing.*;
+import java.util.Map;
+
+/**
+ * author: liana
+ * data: 7/21/14.
+ */
+abstract public class StudyTaskNavigationAction extends DumbAwareAction {
+ public void navigateTask(Project project) {
+ Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+ FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+ assert selectedEditor != null;
+ VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ assert openedFile != null;
+ TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+ assert selectedTaskFile != null;
+ Task currentTask = selectedTaskFile.getTask();
+ Task nextTask = getTargetTask(currentTask);
+ if (nextTask == null) {
+ BalloonBuilder balloonBuilder =
+ JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(getNavigationFinishedMessage(), MessageType.INFO, null);
+ Balloon balloon = balloonBuilder.createBalloon();
+ StudyEditor selectedStudyEditor = StudyEditor.getSelectedStudyEditor(project);
+ balloon.showInCenterOf(getButton(selectedStudyEditor));
+ return;
+ }
+ for (VirtualFile file : FileEditorManager.getInstance(project).getOpenFiles()) {
+ FileEditorManager.getInstance(project).closeFile(file);
+ }
+ int nextTaskIndex = nextTask.getIndex();
+ int lessonIndex = nextTask.getLesson().getIndex();
+ Map<String, TaskFile> nextTaskFiles = nextTask.getTaskFiles();
+ if (nextTaskFiles.isEmpty()) {
+ return;
+ }
+ VirtualFile projectDir = project.getBaseDir();
+ String lessonDirName = Lesson.LESSON_DIR + String.valueOf(lessonIndex + 1);
+ if (projectDir == null) {
+ return;
+ }
+ VirtualFile lessonDir = projectDir.findChild(lessonDirName);
+ if (lessonDir == null) {
+ return;
+ }
+ String taskDirName = Task.TASK_DIR + String.valueOf(nextTaskIndex + 1);
+ VirtualFile taskDir = lessonDir.findChild(taskDirName);
+ if (taskDir == null) {
+ return;
+ }
+ VirtualFile shouldBeActive = null;
+ for (Map.Entry<String, TaskFile> entry : nextTaskFiles.entrySet()) {
+ String name = entry.getKey();
+ TaskFile taskFile = entry.getValue();
+ VirtualFile vf = taskDir.findChild(name);
+ if (vf != null) {
+ FileEditorManager.getInstance(project).openFile(vf, true);
+ if (!taskFile.getTaskWindows().isEmpty()) {
+ shouldBeActive = vf;
+ }
+ }
+ }
+ if (shouldBeActive != null) {
+ FileEditorManager.getInstance(project).openFile(shouldBeActive, true);
+ }
+ }
+
+ protected abstract JButton getButton(StudyEditor selectedStudyEditor);
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ navigateTask(e.getProject());
+ }
+
+ protected abstract String getNavigationFinishedMessage();
+
+ protected abstract Task getTargetTask(Task sourceTask);
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyWindowNavigationAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyWindowNavigationAction.java
new file mode 100644
index 000000000000..8c6b90221555
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyWindowNavigationAction.java
@@ -0,0 +1,65 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+abstract public class StudyWindowNavigationAction extends DumbAwareAction {
+
+ public StudyWindowNavigationAction(String actionId, String description, Icon icon) {
+ super(actionId, description, icon);
+ }
+
+ public void navigateWindow(@NotNull final Project project) {
+ Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+ if (selectedEditor != null) {
+ FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+ VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+ if (openedFile != null) {
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+ if (selectedTaskFile != null) {
+ TaskWindow selectedTaskWindow = selectedTaskFile.getSelectedTaskWindow();
+ if (selectedTaskWindow == null) {
+ return;
+ }
+ TaskWindow nextTaskWindow = getNextTaskWindow(selectedTaskWindow);
+ if (nextTaskWindow == null) {
+ return;
+ }
+ nextTaskWindow.draw(selectedEditor, true, true);
+ selectedTaskFile.setSelectedTaskWindow(nextTaskWindow);
+ }
+ }
+ }
+ }
+
+ @Nullable
+ protected abstract TaskWindow getNextTaskWindow(@NotNull final TaskWindow window);
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ Project project = e.getProject();
+ if (project == null) {
+ return;
+ }
+ navigateWindow(project);
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ StudyUtils.updateAction(e);
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/Course.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Course.java
new file mode 100644
index 000000000000..89613ac7918f
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Course.java
@@ -0,0 +1,104 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Course {
+
+ private static final Logger LOG = Logger.getInstance(Course.class.getName());
+ public static final String PLAYGROUND_DIR = "Playground";
+ public List<Lesson> lessons = new ArrayList<Lesson>();
+ public String description;
+ public String name;
+ public String myResourcePath = "";
+ public String author;
+ public static final String COURSE_DIR = "course";
+ public static final String HINTS_DIR = "hints";
+
+
+ public List<Lesson> getLessons() {
+ return lessons;
+ }
+
+ /**
+ * Initializes state of course
+ */
+ public void init(boolean isRestarted) {
+ for (Lesson lesson : lessons) {
+ lesson.init(this, isRestarted);
+ }
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ /**
+ * Creates course directory in project user created
+ *
+ * @param baseDir project directory
+ * @param resourceRoot directory where original course is stored
+ */
+ public void create(@NotNull final VirtualFile baseDir, @NotNull final File resourceRoot) {
+ ApplicationManager.getApplication().invokeLater(
+ new Runnable() {
+ @Override
+ public void run() {
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ for (int i = 0; i < lessons.size(); i++) {
+ Lesson lesson = lessons.get(i);
+ lesson.setIndex(i);
+ lesson.create(baseDir, resourceRoot);
+ }
+ baseDir.createChildDirectory(this, PLAYGROUND_DIR);
+ File[] files = resourceRoot.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return !name.contains(Lesson.LESSON_DIR) && !name.equals("course.json") && !name.equals("hints");
+ }
+ });
+ for (File file: files) {
+ FileUtil.copy(file, new File(baseDir.getPath(), file.getName()));
+ }
+ }
+ catch (IOException e) {
+ LOG.error(e);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setResourcePath(@NotNull final String resourcePath) {
+ myResourcePath = resourcePath;
+ }
+
+ public String getResourcePath() {
+ return myResourcePath;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/CourseInfo.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/CourseInfo.java
new file mode 100644
index 000000000000..9f820c12c572
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/CourseInfo.java
@@ -0,0 +1,52 @@
+package com.jetbrains.python.edu.course;
+
+/**
+ * Implementation of class which contains information to be shawn in course description in tool window
+ * and when project is being created
+ */
+public class CourseInfo {
+ private String myName;
+ private String myAuthor;
+ private String myDescription;
+ public static CourseInfo INVALID_COURSE = new CourseInfo("", "", "");
+
+ public CourseInfo(String name, String author, String description) {
+ myName = name;
+ myAuthor = author;
+ myDescription = description;
+ }
+
+ public String getName() {
+ return myName;
+ }
+
+ public String getAuthor() {
+ return myAuthor;
+ }
+
+ public String getDescription() {
+ return myDescription;
+ }
+
+ @Override
+ public String toString() {
+ return myName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CourseInfo that = (CourseInfo)o;
+ return that.getName().equals(myName) && that.getAuthor().equals(myAuthor)
+ && that.getDescription().equals(myDescription);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = myName != null ? myName.hashCode() : 0;
+ result = 31 * result + (myAuthor != null ? myAuthor.hashCode() : 0);
+ result = 31 * result + (myDescription != null ? myDescription.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/Lesson.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Lesson.java
new file mode 100644
index 000000000000..3879d519957e
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Lesson.java
@@ -0,0 +1,109 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xmlb.annotations.Transient;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Lesson implements Stateful{
+ public String name;
+ public List<Task> taskList = new ArrayList<Task>();
+ private Course myCourse = null;
+ public int myIndex = -1;
+ public static final String LESSON_DIR = "lesson";
+ public LessonInfo myLessonInfo = new LessonInfo();
+
+ public LessonInfo getLessonInfo() {
+ return myLessonInfo;
+ }
+
+ @Transient
+ public StudyStatus getStatus() {
+ for (Task task : taskList) {
+ StudyStatus taskStatus = task.getStatus();
+ if (taskStatus == StudyStatus.Unchecked || taskStatus == StudyStatus.Failed) {
+ return StudyStatus.Unchecked;
+ }
+ }
+ return StudyStatus.Solved;
+ }
+
+ @Override
+ public void setStatus(StudyStatus status, StudyStatus oldStatus) {
+ for (Task task : taskList) {
+ task.setStatus(status, oldStatus);
+ }
+ }
+
+ public List<Task> getTaskList() {
+ return taskList;
+ }
+
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Creates lesson directory in its course folder in project user created
+ *
+ * @param courseDir project directory of course
+ * @param resourceRoot directory where original lesson stored
+ * @throws java.io.IOException
+ */
+ public void create(@NotNull final VirtualFile courseDir, @NotNull final File resourceRoot) throws IOException {
+ String lessonDirName = LESSON_DIR + Integer.toString(myIndex + 1);
+ VirtualFile lessonDir = courseDir.createChildDirectory(this, lessonDirName);
+ for (int i = 0; i < taskList.size(); i++) {
+ Task task = taskList.get(i);
+ task.setIndex(i);
+ task.create(lessonDir, new File(resourceRoot, lessonDir.getName()));
+ }
+ }
+
+
+ /**
+ * Initializes state of lesson
+ *
+ * @param course course which lesson belongs to
+ */
+ public void init(final Course course, boolean isRestarted) {
+ myCourse = course;
+ myLessonInfo.setTaskNum(taskList.size());
+ myLessonInfo.setTaskUnchecked(taskList.size());
+ for (Task task : taskList) {
+ task.init(this, isRestarted);
+ }
+ }
+
+ public Lesson next() {
+ List<Lesson> lessons = myCourse.getLessons();
+ if (myIndex + 1 >= lessons.size()) {
+ return null;
+ }
+ return lessons.get(myIndex + 1);
+ }
+
+ public void setIndex(int index) {
+ myIndex = index;
+ }
+
+ public int getIndex() {
+ return myIndex;
+ }
+
+ public Lesson prev() {
+ if (myIndex - 1 < 0) {
+ return null;
+ }
+ return myCourse.getLessons().get(myIndex - 1);
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/LessonInfo.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/LessonInfo.java
new file mode 100644
index 000000000000..85e2eb8be1a9
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/LessonInfo.java
@@ -0,0 +1,60 @@
+package com.jetbrains.python.edu.course;
+
+/**
+ * Implementation of class which contains information about student progress in current lesson
+ */
+public class LessonInfo {
+ private int myTaskNum;
+ private int myTaskFailed;
+ private int myTaskSolved;
+ private int myTaskUnchecked;
+
+ public int getTaskNum() {
+ return myTaskNum;
+ }
+
+ public void setTaskNum(int taskNum) {
+ myTaskNum = taskNum;
+ }
+
+ public int getTaskFailed() {
+ return myTaskFailed;
+ }
+
+ public void setTaskFailed(int taskFailed) {
+ myTaskFailed = taskFailed;
+ }
+
+ public int getTaskSolved() {
+ return myTaskSolved;
+ }
+
+ public void setTaskSolved(int taskSolved) {
+ myTaskSolved = taskSolved;
+ }
+
+ public int getTaskUnchecked() {
+ return myTaskUnchecked;
+ }
+
+ public void setTaskUnchecked(int taskUnchecked) {
+ myTaskUnchecked = taskUnchecked;
+ }
+
+ public void update(StudyStatus status, int delta) {
+ switch (status) {
+ case Solved: {
+ myTaskSolved += delta;
+ break;
+ }
+ case Failed: {
+ myTaskFailed += delta;
+ break;
+ }
+ case Unchecked: {
+ myTaskUnchecked += delta;
+ break;
+ }
+ }
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/Stateful.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Stateful.java
new file mode 100644
index 000000000000..3a163622f56d
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Stateful.java
@@ -0,0 +1,6 @@
+package com.jetbrains.python.edu.course;
+
+public interface Stateful {
+ StudyStatus getStatus();
+ void setStatus(StudyStatus status, StudyStatus oldStatus);
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/StudyStatus.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/StudyStatus.java
new file mode 100644
index 000000000000..d95b42b73866
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/StudyStatus.java
@@ -0,0 +1,8 @@
+package com.jetbrains.python.edu.course;
+
+/**
+ * @see {@link TaskWindow#myStatus}
+ */
+public enum StudyStatus {
+ Unchecked, Solved, Failed
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/Task.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Task.java
new file mode 100644
index 000000000000..2323412f4374
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Task.java
@@ -0,0 +1,201 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xmlb.annotations.Transient;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import com.jetbrains.python.edu.StudyUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of task which contains task files, tests, input file for tests
+ */
+public class Task implements Stateful{
+ public static final String TASK_DIR = "task";
+ private static final String ourTestFile = "tests.py";
+ public String name;
+ private static final String ourTextFile = "task.html";
+ public Map<String, TaskFile> taskFiles = new HashMap<String, TaskFile>();
+ private Lesson myLesson;
+ public int myIndex;
+ public List<UserTest> userTests = new ArrayList<UserTest>();
+ public static final String USER_TESTS = "userTests";
+
+ public Map<String, TaskFile> getTaskFiles() {
+ return taskFiles;
+ }
+
+ @Transient
+ public StudyStatus getStatus() {
+ for (TaskFile taskFile : taskFiles.values()) {
+ StudyStatus taskFileStatus = taskFile.getStatus();
+ if (taskFileStatus == StudyStatus.Unchecked) {
+ return StudyStatus.Unchecked;
+ }
+ if (taskFileStatus == StudyStatus.Failed) {
+ return StudyStatus.Failed;
+ }
+ }
+ return StudyStatus.Solved;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setStatus(@NotNull final StudyStatus status, @NotNull final StudyStatus oldStatus) {
+ LessonInfo lessonInfo = myLesson.getLessonInfo();
+ if (status != oldStatus) {
+ lessonInfo.update(oldStatus, -1);
+ lessonInfo.update(status, +1);
+ }
+ for (TaskFile taskFile : taskFiles.values()) {
+ taskFile.setStatus(status, oldStatus);
+ }
+ }
+
+ public List<UserTest> getUserTests() {
+ return userTests;
+ }
+
+ public String getTestFile() {
+ return ourTestFile;
+ }
+
+ public String getText() {
+ return ourTextFile;
+ }
+
+ /**
+ * Creates task directory in its lesson folder in project user created
+ *
+ * @param lessonDir project directory of lesson which task belongs to
+ * @param resourceRoot directory where original task file stored
+ * @throws java.io.IOException
+ */
+ public void create(@NotNull final VirtualFile lessonDir, @NotNull final File resourceRoot) throws IOException {
+ VirtualFile taskDir = lessonDir.createChildDirectory(this, TASK_DIR + Integer.toString(myIndex + 1));
+ File newResourceRoot = new File(resourceRoot, taskDir.getName());
+ int i = 0;
+ for (Map.Entry<String, TaskFile> taskFile : taskFiles.entrySet()) {
+ TaskFile taskFileContent = taskFile.getValue();
+ taskFileContent.setIndex(i);
+ i++;
+ taskFileContent.create(taskDir, newResourceRoot, taskFile.getKey());
+ }
+ File[] filesInTask = newResourceRoot.listFiles();
+ if (filesInTask != null) {
+ for (File file : filesInTask) {
+ String fileName = file.getName();
+ if (!isTaskFile(fileName)) {
+ File resourceFile = new File(newResourceRoot, fileName);
+ File fileInProject = new File(taskDir.getCanonicalPath(), fileName);
+ FileUtil.copy(resourceFile, fileInProject);
+ }
+ }
+ }
+ }
+
+ private boolean isTaskFile(@NotNull final String fileName) {
+ return taskFiles.get(fileName) != null;
+ }
+
+ @Nullable
+ public TaskFile getFile(@NotNull final String fileName) {
+ return taskFiles.get(fileName);
+ }
+
+ /**
+ * Initializes state of task file
+ *
+ * @param lesson lesson which task belongs to
+ */
+ public void init(final Lesson lesson, boolean isRestarted) {
+ myLesson = lesson;
+ for (TaskFile taskFile : taskFiles.values()) {
+ taskFile.init(this, isRestarted);
+ }
+ }
+
+ public Task next() {
+ Lesson currentLesson = this.myLesson;
+ List<Task> taskList = myLesson.getTaskList();
+ if (myIndex + 1 < taskList.size()) {
+ return taskList.get(myIndex + 1);
+ }
+ Lesson nextLesson = currentLesson.next();
+ if (nextLesson == null) {
+ return null;
+ }
+ return StudyUtils.getFirst(nextLesson.getTaskList());
+ }
+
+ public Task prev() {
+ Lesson currentLesson = this.myLesson;
+ if (myIndex - 1 >= 0) {
+ return myLesson.getTaskList().get(myIndex - 1);
+ }
+ Lesson prevLesson = currentLesson.prev();
+ if (prevLesson == null) {
+ return null;
+ }
+ //getting last task in previous lesson
+ return prevLesson.getTaskList().get(prevLesson.getTaskList().size() - 1);
+ }
+
+ public void setIndex(int index) {
+ myIndex = index;
+ }
+
+ public int getIndex() {
+ return myIndex;
+ }
+
+ public Lesson getLesson() {
+ return myLesson;
+ }
+
+
+ @Nullable
+ public VirtualFile getTaskDir(Project project) {
+ String lessonDirName = Lesson.LESSON_DIR + String.valueOf(myLesson.getIndex() + 1);
+ String taskDirName = TASK_DIR + String.valueOf(myIndex + 1);
+ VirtualFile courseDir = project.getBaseDir();
+ if (courseDir != null) {
+ VirtualFile lessonDir = courseDir.findChild(lessonDirName);
+ if (lessonDir != null) {
+ return lessonDir.findChild(taskDirName);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets text of resource file such as test input file or task text in needed format
+ *
+ * @param fileName name of resource file which should exist in task directory
+ * @param wrapHTML if it's necessary to wrap text with html tags
+ * @return text of resource file wrapped with html tags if necessary
+ */
+ @Nullable
+ public String getResourceText(@NotNull final Project project, @NotNull final String fileName, boolean wrapHTML) {
+ VirtualFile taskDir = getTaskDir(project);
+ if (taskDir != null) {
+ return StudyUtils.getFileText(taskDir.getCanonicalPath(), fileName, wrapHTML);
+ }
+ return null;
+ }
+
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskFile.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskFile.java
new file mode 100644
index 000000000000..4f17fc0d27f3
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskFile.java
@@ -0,0 +1,228 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xmlb.annotations.Transient;
+import com.jetbrains.python.edu.StudyUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of task file which contains task windows for student to type in and
+ * which is visible to student in project view
+ */
+
+public class TaskFile implements Stateful{
+ public List<TaskWindow> taskWindows = new ArrayList<TaskWindow>();
+ private Task myTask;
+ @Transient
+ private TaskWindow mySelectedTaskWindow = null;
+ public int myIndex = -1;
+ private boolean myUserCreated = false;
+
+ /**
+ * @return if all the windows in task file are marked as resolved
+ */
+ @Transient
+ public StudyStatus getStatus() {
+ for (TaskWindow taskWindow : taskWindows) {
+ StudyStatus windowStatus = taskWindow.getStatus();
+ if (windowStatus == StudyStatus.Failed) {
+ return StudyStatus.Failed;
+ }
+ if (windowStatus == StudyStatus.Unchecked) {
+ return StudyStatus.Unchecked;
+ }
+ }
+ return StudyStatus.Solved;
+ }
+
+ public Task getTask() {
+ return myTask;
+ }
+
+ @Nullable
+ @Transient
+ public TaskWindow getSelectedTaskWindow() {
+ return mySelectedTaskWindow;
+ }
+
+ /**
+ * @param selectedTaskWindow window from this task file to be set as selected
+ */
+ public void setSelectedTaskWindow(@NotNull final TaskWindow selectedTaskWindow) {
+ if (selectedTaskWindow.getTaskFile() == this) {
+ mySelectedTaskWindow = selectedTaskWindow;
+ }
+ else {
+ throw new IllegalArgumentException("Window may be set as selected only in task file which it belongs to");
+ }
+ }
+
+ public List<TaskWindow> getTaskWindows() {
+ return taskWindows;
+ }
+
+ /**
+ * Creates task files in its task folder in project user created
+ *
+ * @param taskDir project directory of task which task file belongs to
+ * @param resourceRoot directory where original task file stored
+ * @throws java.io.IOException
+ */
+ public void create(@NotNull final VirtualFile taskDir, @NotNull final File resourceRoot,
+ @NotNull final String name) throws IOException {
+ String systemIndependentName = FileUtil.toSystemIndependentName(name);
+ final int index = systemIndependentName.lastIndexOf("/");
+ if (index > 0) {
+ systemIndependentName = systemIndependentName.substring(index + 1);
+ }
+ File resourceFile = new File(resourceRoot, name);
+ File fileInProject = new File(taskDir.getPath(), systemIndependentName);
+ FileUtil.copy(resourceFile, fileInProject);
+ }
+
+ public void drawAllWindows(Editor editor) {
+ for (TaskWindow taskWindow : taskWindows) {
+ taskWindow.draw(editor, false, false);
+ }
+ }
+
+
+ /**
+ * @param pos position in editor
+ * @return task window located in specified position or null if there is no task window in this position
+ */
+ @Nullable
+ public TaskWindow getTaskWindow(@NotNull final Document document, @NotNull final LogicalPosition pos) {
+ int line = pos.line;
+ if (line >= document.getLineCount()) {
+ return null;
+ }
+ int column = pos.column;
+ int offset = document.getLineStartOffset(line) + column;
+ for (TaskWindow tw : taskWindows) {
+ if (tw.getLine() <= line) {
+ int twStartOffset = tw.getRealStartOffset(document);
+ final int length = tw.getLength() > 0 ? tw.getLength() : 0;
+ int twEndOffset = twStartOffset + length;
+ if (twStartOffset <= offset && offset <= twEndOffset) {
+ return tw;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates task window lines
+ *
+ * @param startLine lines greater than this line and including this line will be updated
+ * @param change change to be added to line numbers
+ */
+ public void incrementLines(int startLine, int change) {
+ for (TaskWindow taskTaskWindow : taskWindows) {
+ if (taskTaskWindow.getLine() >= startLine) {
+ taskTaskWindow.setLine(taskTaskWindow.getLine() + change);
+ }
+ }
+ }
+
+ /**
+ * Initializes state of task file
+ *
+ * @param task task which task file belongs to
+ */
+
+ public void init(final Task task, boolean isRestarted) {
+ myTask = task;
+ for (TaskWindow taskWindow : taskWindows) {
+ taskWindow.init(this, isRestarted);
+ }
+ Collections.sort(taskWindows);
+ for (int i = 0; i < taskWindows.size(); i++) {
+ taskWindows.get(i).setIndex(i);
+ }
+ }
+
+ /**
+ * @param index index of task file in list of task files of its task
+ */
+ public void setIndex(int index) {
+ myIndex = index;
+ }
+
+ /**
+ * Updates windows in specific line
+ *
+ * @param lineChange change in line number
+ * @param line line to be updated
+ * @param newEndOffsetInLine distance from line start to end of inserted fragment
+ * @param oldEndOffsetInLine distance from line start to end of changed fragment
+ */
+ public void updateLine(int lineChange, int line, int newEndOffsetInLine, int oldEndOffsetInLine) {
+ for (TaskWindow w : taskWindows) {
+ if ((w.getLine() == line) && (w.getStart() >= oldEndOffsetInLine)) {
+ int distance = w.getStart() - oldEndOffsetInLine;
+ if (lineChange != 0 || newEndOffsetInLine <= w.getStart()) {
+ w.setStart(distance + newEndOffsetInLine);
+ w.setLine(line + lineChange);
+ }
+ }
+ }
+ }
+
+ public static void copy(@NotNull final TaskFile source, @NotNull final TaskFile target) {
+ List<TaskWindow> sourceTaskWindows = source.getTaskWindows();
+ List<TaskWindow> windowsCopy = new ArrayList<TaskWindow>(sourceTaskWindows.size());
+ for (TaskWindow taskWindow : sourceTaskWindows) {
+ TaskWindow taskWindowCopy = new TaskWindow();
+ taskWindowCopy.setLine(taskWindow.getLine());
+ taskWindowCopy.setStart(taskWindow.getStart());
+ taskWindowCopy.setLength(taskWindow.getLength());
+ taskWindowCopy.setPossibleAnswer(taskWindow.getPossibleAnswer());
+ taskWindowCopy.setIndex(taskWindow.getIndex());
+ windowsCopy.add(taskWindowCopy);
+ }
+ target.setTaskWindows(windowsCopy);
+ }
+
+ public void setTaskWindows(List<TaskWindow> taskWindows) {
+ this.taskWindows = taskWindows;
+ }
+
+ public void setStatus(@NotNull final StudyStatus status, @NotNull final StudyStatus oldStatus) {
+ for (TaskWindow taskWindow : taskWindows) {
+ taskWindow.setStatus(status, oldStatus);
+ }
+ }
+
+ public void setUserCreated(boolean userCreated) {
+ myUserCreated = userCreated;
+ }
+
+ public boolean isUserCreated() {
+ return myUserCreated;
+ }
+
+ public void navigateToFirstTaskWindow(@NotNull final Editor editor) {
+ if (!taskWindows.isEmpty()) {
+ TaskWindow firstTaskWindow = StudyUtils.getFirst(taskWindows);
+ mySelectedTaskWindow = firstTaskWindow;
+ LogicalPosition taskWindowStart = new LogicalPosition(firstTaskWindow.getLine(), firstTaskWindow.getStart());
+ editor.getCaretModel().moveToLogicalPosition(taskWindowStart);
+ int startOffset = firstTaskWindow.getRealStartOffset(editor.getDocument());
+ int endOffset = startOffset + firstTaskWindow.getLength();
+ editor.getSelectionModel().setSelection(startOffset, endOffset);
+ }
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskWindow.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskWindow.java
new file mode 100644
index 000000000000..4fb112cc1f9b
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskWindow.java
@@ -0,0 +1,177 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.colors.EditorColors;
+import com.intellij.openapi.editor.colors.EditorColorsManager;
+import com.intellij.openapi.editor.markup.HighlighterLayer;
+import com.intellij.openapi.editor.markup.HighlighterTargetArea;
+import com.intellij.openapi.editor.markup.RangeHighlighter;
+import com.intellij.openapi.editor.markup.TextAttributes;
+import com.intellij.ui.JBColor;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Implementation of windows which user should type in
+ */
+
+
+public class TaskWindow implements Comparable, Stateful {
+
+ public int line = 0;
+ public int start = 0;
+ public String hint = "";
+ public String possibleAnswer = "";
+ public int length = 0;
+ private TaskFile myTaskFile;
+ public int myIndex = -1;
+ public int myInitialLine = -1;
+ public int myInitialStart = -1;
+ public int myInitialLength = -1;
+ public StudyStatus myStatus = StudyStatus.Unchecked;
+
+ public StudyStatus getStatus() {
+ return myStatus;
+ }
+
+ public void setStatus(StudyStatus status, StudyStatus oldStatus) {
+ myStatus = status;
+ }
+
+ public void setIndex(int index) {
+ myIndex = index;
+ }
+
+ public int getLength() {
+ return length;
+ }
+
+ public void setLength(int length) {
+ this.length = length;
+ }
+
+ public int getStart() {
+ return start;
+ }
+
+ public void setStart(int start) {
+ this.start = start;
+ }
+
+ public void setLine(int line) {
+ this.line = line;
+ }
+
+ public int getLine() {
+ return line;
+ }
+
+
+ /**
+ * Draw task window with color according to its status
+ */
+ public void draw(@NotNull final Editor editor, boolean drawSelection, boolean moveCaret) {
+ Document document = editor.getDocument();
+ if (!isValid(document)) {
+ return;
+ }
+ TextAttributes defaultTestAttributes =
+ EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.LIVE_TEMPLATE_ATTRIBUTES);
+ JBColor color = getColor();
+ int startOffset = document.getLineStartOffset(line) + start;
+ RangeHighlighter
+ rh = editor.getMarkupModel().addRangeHighlighter(startOffset, startOffset + length, HighlighterLayer.LAST + 1,
+ new TextAttributes(defaultTestAttributes.getForegroundColor(),
+ defaultTestAttributes.getBackgroundColor(), color,
+ defaultTestAttributes.getEffectType(),
+ defaultTestAttributes.getFontType()),
+ HighlighterTargetArea.EXACT_RANGE);
+ if (drawSelection) {
+ editor.getSelectionModel().setSelection(startOffset, startOffset + length);
+ }
+ if (moveCaret) {
+ editor.getCaretModel().moveToOffset(startOffset);
+ }
+ rh.setGreedyToLeft(true);
+ rh.setGreedyToRight(true);
+ }
+
+ public boolean isValid(@NotNull final Document document) {
+ boolean isLineValid = line < document.getLineCount() && line >= 0;
+ if (!isLineValid) return false;
+ boolean isStartValid = start >= 0 && start < document.getLineEndOffset(line);
+ boolean isLengthValid = (getRealStartOffset(document) + length) <= document.getTextLength();
+ return isLengthValid && isStartValid;
+ }
+
+ private JBColor getColor() {
+ if (myStatus == StudyStatus.Solved) {
+ return JBColor.GREEN;
+ }
+ if (myStatus == StudyStatus.Failed) {
+ return JBColor.RED;
+ }
+ return JBColor.BLUE;
+ }
+
+ public int getRealStartOffset(@NotNull final Document document) {
+ return document.getLineStartOffset(line) + start;
+ }
+
+ /**
+ * Initializes window
+ *
+ * @param file task file which window belongs to
+ */
+ public void init(final TaskFile file, boolean isRestarted) {
+ if (!isRestarted) {
+ myInitialLine = line;
+ myInitialLength = length;
+ myInitialStart = start;
+ }
+ myTaskFile = file;
+ }
+
+ public TaskFile getTaskFile() {
+ return myTaskFile;
+ }
+
+ @Override
+ public int compareTo(@NotNull Object o) {
+ TaskWindow taskWindow = (TaskWindow)o;
+ if (taskWindow.getTaskFile() != myTaskFile) {
+ throw new ClassCastException();
+ }
+ int lineDiff = line - taskWindow.line;
+ if (lineDiff == 0) {
+ return start - taskWindow.start;
+ }
+ return lineDiff;
+ }
+
+ /**
+ * Returns window to its initial state
+ */
+ public void reset() {
+ myStatus = StudyStatus.Unchecked;
+ line = myInitialLine;
+ start = myInitialStart;
+ length = myInitialLength;
+ }
+
+ public String getHint() {
+ return hint;
+ }
+
+ public String getPossibleAnswer() {
+ return possibleAnswer;
+ }
+
+ public void setPossibleAnswer(String possibleAnswer) {
+ this.possibleAnswer = possibleAnswer;
+ }
+
+ public int getIndex() {
+ return myIndex;
+ }
+} \ No newline at end of file
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/UserTest.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/UserTest.java
new file mode 100644
index 000000000000..8133e9102a14
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/UserTest.java
@@ -0,0 +1,41 @@
+package com.jetbrains.python.edu.course;
+
+public class UserTest {
+ private String input;
+ private String output;
+ private StringBuilder myInputBuffer = new StringBuilder();
+ private StringBuilder myOutputBuffer = new StringBuilder();
+ private boolean myEditable = false;
+
+ public String getInput() {
+ return input;
+ }
+
+ public void setInput(String input) {
+ this.input = input;
+ }
+
+ public String getOutput() {
+ return output;
+ }
+
+ public void setOutput(String output) {
+ this.output = output;
+ }
+
+ public StringBuilder getInputBuffer() {
+ return myInputBuffer;
+ }
+
+ public StringBuilder getOutputBuffer() {
+ return myOutputBuffer;
+ }
+
+ public boolean isEditable() {
+ return myEditable;
+ }
+
+ public void setEditable(boolean editable) {
+ myEditable = editable;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyEditor.java b/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyEditor.java
new file mode 100644
index 000000000000..69c5acc5f127
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyEditor.java
@@ -0,0 +1,347 @@
+package com.jetbrains.python.edu.editor;
+
+import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.EditorFactory;
+import com.intellij.openapi.editor.colors.EditorColorsManager;
+import com.intellij.openapi.editor.colors.EditorColorsScheme;
+import com.intellij.openapi.editor.impl.DocumentImpl;
+import com.intellij.openapi.fileEditor.*;
+import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
+import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorImpl;
+import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.pom.Navigatable;
+import com.intellij.ui.HideableTitledPanel;
+import com.intellij.ui.JBColor;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.python.edu.StudyDocumentListener;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.actions.*;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.text.MutableAttributeSet;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyledDocument;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeListener;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of StudyEditor which has panel with special buttons and task text
+ * also @see {@link com.jetbrains.python.edu.editor.StudyFileEditorProvider}
+ */
+public class StudyEditor implements TextEditor {
+ private static final String TASK_TEXT_HEADER = "Task Text";
+ private final FileEditor myDefaultEditor;
+ private final JComponent myComponent;
+ private JButton myCheckButton;
+ private JButton myNextTaskButton;
+ private JButton myPrevTaskButton;
+ private JButton myRefreshButton;
+ private static final Map<Document, StudyDocumentListener> myDocumentListeners = new HashMap<Document, StudyDocumentListener>();
+ private Project myProject;
+
+ public JButton getCheckButton() {
+ return myCheckButton;
+ }
+
+ public JButton getPrevTaskButton() {
+ return myPrevTaskButton;
+ }
+
+ private static JButton addButton(@NotNull final JComponent parentComponent, String toolTipText, Icon icon) {
+ JButton newButton = new JButton();
+ newButton.setToolTipText(toolTipText);
+ newButton.setIcon(icon);
+ newButton.setSize(new Dimension(icon.getIconWidth(), icon.getIconHeight()));
+ parentComponent.add(newButton);
+ return newButton;
+ }
+
+ public static void addDocumentListener(@NotNull final Document document, @NotNull final StudyDocumentListener listener) {
+ myDocumentListeners.put(document, listener);
+ }
+
+ @Nullable
+ public static StudyDocumentListener getListener(@NotNull final Document document) {
+ return myDocumentListeners.get(document);
+ }
+
+ public StudyEditor(@NotNull final Project project, @NotNull final VirtualFile file) {
+ myProject = project;
+ myDefaultEditor = TextEditorProvider.getInstance().createEditor(myProject, file);
+ myComponent = myDefaultEditor.getComponent();
+ JPanel studyPanel = new JPanel();
+ studyPanel.setLayout(new BoxLayout(studyPanel, BoxLayout.Y_AXIS));
+ TaskFile taskFile = StudyTaskManager.getInstance(myProject).getTaskFile(file);
+ if (taskFile != null) {
+ Task currentTask = taskFile.getTask();
+ String taskText = currentTask.getResourceText(project, currentTask.getText(), false);
+ initializeTaskText(studyPanel, taskText);
+ JPanel studyButtonPanel = new JPanel(new GridLayout(1, 2));
+ JPanel taskActionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ studyButtonPanel.add(taskActionsPanel);
+ studyButtonPanel.add(new JPanel());
+ initializeButtons(taskActionsPanel, taskFile);
+ studyPanel.add(studyButtonPanel);
+ myComponent.add(studyPanel, BorderLayout.NORTH);
+ }
+ }
+
+ private static void initializeTaskText(JPanel studyPanel, @Nullable String taskText) {
+ JTextPane taskTextPane = new JTextPane();
+ taskTextPane.setContentType("text/html");
+ taskTextPane.setEditable(false);
+ taskTextPane.setText(taskText);
+ EditorColorsScheme editorColorsScheme = EditorColorsManager.getInstance().getGlobalScheme();
+ int fontSize = editorColorsScheme.getEditorFontSize();
+ String fontName = editorColorsScheme.getEditorFontName();
+ setJTextPaneFont(taskTextPane, new Font(fontName, Font.PLAIN, fontSize), JBColor.BLACK);
+ taskTextPane.setBackground(UIUtil.getPanelBackground());
+ taskTextPane.setBorder(new EmptyBorder(15, 20, 0, 100));
+ HideableTitledPanel taskTextPanel = new HideableTitledPanel(TASK_TEXT_HEADER, taskTextPane, true);
+ taskTextPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
+ studyPanel.add(taskTextPanel);
+ }
+
+ private static void setJTextPaneFont(JTextPane jtp, Font font, Color c) {
+ MutableAttributeSet attrs = jtp.getInputAttributes();
+ StyleConstants.setFontFamily(attrs, font.getFamily());
+ StyleConstants.setFontSize(attrs, font.getSize());
+ StyleConstants.setItalic(attrs, (font.getStyle() & Font.ITALIC) != 0);
+ StyleConstants.setBold(attrs, (font.getStyle() & Font.BOLD) != 0);
+ StyleConstants.setForeground(attrs, c);
+ StyledDocument doc = jtp.getStyledDocument();
+ doc.setCharacterAttributes(0, doc.getLength() + 1, attrs, false);
+ }
+
+ private void initializeButtons(@NotNull final JPanel taskActionsPanel, @NotNull final TaskFile taskFile) {
+ myCheckButton = addButton(taskActionsPanel, "Check task", StudyIcons.Resolve);
+ myPrevTaskButton = addButton(taskActionsPanel, "Prev Task", StudyIcons.Prev);
+ myNextTaskButton = addButton(taskActionsPanel, "Next Task", StudyIcons.Next);
+ myRefreshButton = addButton(taskActionsPanel, "Start task again", StudyIcons.Refresh24);
+ if (!taskFile.getTask().getUserTests().isEmpty()) {
+ JButton runButton = addButton(taskActionsPanel, "Run", StudyIcons.Run);
+ runButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ StudyRunAction studyRunAction = (StudyRunAction)ActionManager.getInstance().getAction("StudyRunAction");
+ studyRunAction.run(myProject);
+ }
+ });
+ JButton watchInputButton = addButton(taskActionsPanel, "Watch test input", StudyIcons.WatchInput);
+ watchInputButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ StudyEditInputAction studyEditInputAction = (StudyEditInputAction)ActionManager.getInstance().getAction("WatchInputAction");
+ studyEditInputAction.showInput(myProject);
+ }
+ });
+ }
+ myCheckButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ StudyCheckAction studyCheckAction = (StudyCheckAction)ActionManager.getInstance().getAction("CheckAction");
+ studyCheckAction.check(myProject);
+ }
+ });
+
+ myNextTaskButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ StudyNextStudyTaskAction studyNextTaskAction = (StudyNextStudyTaskAction)ActionManager.getInstance().getAction("NextTaskAction");
+ studyNextTaskAction.navigateTask(myProject);
+ }
+ });
+ myPrevTaskButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ StudyPreviousStudyTaskAction
+ prevTaskAction = (StudyPreviousStudyTaskAction)ActionManager.getInstance().getAction("PreviousTaskAction");
+ prevTaskAction.navigateTask(myProject);
+ }
+ });
+ myRefreshButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ StudyRefreshTaskAction studyRefreshTaskAction = (StudyRefreshTaskAction)ActionManager.getInstance().getAction("RefreshTaskAction");
+ studyRefreshTaskAction.refresh(myProject);
+ }
+ });
+ }
+
+ public JButton getNextTaskButton() {
+ return myNextTaskButton;
+ }
+
+ public JButton getRefreshButton() {
+ return myRefreshButton;
+ }
+
+ FileEditor getDefaultEditor() {
+ return myDefaultEditor;
+ }
+
+ @NotNull
+ @Override
+ public JComponent getComponent() {
+ return myComponent;
+ }
+
+ @Nullable
+ @Override
+ public JComponent getPreferredFocusedComponent() {
+ return myDefaultEditor.getPreferredFocusedComponent();
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "Study Editor";
+ }
+
+ @NotNull
+ @Override
+ public FileEditorState getState(@NotNull FileEditorStateLevel level) {
+ return myDefaultEditor.getState(level);
+ }
+
+ @Override
+ public void setState(@NotNull FileEditorState state) {
+ myDefaultEditor.setState(state);
+ }
+
+ @Override
+ public boolean isModified() {
+ return myDefaultEditor.isModified();
+ }
+
+ @Override
+ public boolean isValid() {
+ return myDefaultEditor.isValid();
+ }
+
+ @Override
+ public void selectNotify() {
+ myDefaultEditor.selectNotify();
+ }
+
+ @Override
+ public void deselectNotify() {
+ myDefaultEditor.deselectNotify();
+ }
+
+ @Override
+ public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
+ myDefaultEditor.addPropertyChangeListener(listener);
+ }
+
+ @Override
+ public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
+ myDefaultEditor.removePropertyChangeListener(listener);
+ }
+
+ @Nullable
+ @Override
+ public BackgroundEditorHighlighter getBackgroundHighlighter() {
+ return myDefaultEditor.getBackgroundHighlighter();
+ }
+
+ @Nullable
+ @Override
+ public FileEditorLocation getCurrentLocation() {
+ return myDefaultEditor.getCurrentLocation();
+ }
+
+ @Nullable
+ @Override
+ public StructureViewBuilder getStructureViewBuilder() {
+ return myDefaultEditor.getStructureViewBuilder();
+ }
+
+ @Override
+ public void dispose() {
+ Disposer.dispose(myDefaultEditor);
+ }
+
+ @Nullable
+ @Override
+ public <T> T getUserData(@NotNull Key<T> key) {
+ return myDefaultEditor.getUserData(key);
+ }
+
+ @Override
+ public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
+ myDefaultEditor.putUserData(key, value);
+ }
+
+
+ @Nullable
+ public static StudyEditor getSelectedStudyEditor(@NotNull final Project project) {
+ try {
+ FileEditor fileEditor = FileEditorManagerEx.getInstanceEx(project).getSplitters().getCurrentWindow().
+ getSelectedEditor().getSelectedEditorWithProvider().getFirst();
+ if (fileEditor instanceof StudyEditor) {
+ return (StudyEditor)fileEditor;
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ return null;
+ }
+
+ @Nullable
+ public static Editor getSelectedEditor(@NotNull final Project project) {
+ StudyEditor studyEditor = getSelectedStudyEditor(project);
+ if (studyEditor != null) {
+ FileEditor defaultEditor = studyEditor.getDefaultEditor();
+ if (defaultEditor instanceof PsiAwareTextEditorImpl) {
+ return ((PsiAwareTextEditorImpl)defaultEditor).getEditor();
+ }
+ }
+ return null;
+ }
+
+ public static void removeListener(Document document) {
+ myDocumentListeners.remove(document);
+ }
+
+ @NotNull
+ @Override
+ public Editor getEditor() {
+ if (myDefaultEditor instanceof TextEditor)
+ return ((TextEditor)myDefaultEditor).getEditor();
+ return EditorFactory.getInstance().createViewer(new DocumentImpl(""), myProject);
+ }
+
+ @Override
+ public boolean canNavigateTo(@NotNull Navigatable navigatable) {
+ if (myDefaultEditor instanceof TextEditor) {
+ ((TextEditor)myDefaultEditor).canNavigateTo(navigatable);
+ }
+ return false;
+ }
+
+ @Override
+ public void navigateTo(@NotNull Navigatable navigatable) {
+ if (myDefaultEditor instanceof TextEditor) {
+ ((TextEditor)myDefaultEditor).navigateTo(navigatable);
+ }
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyFileEditorProvider.java b/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyFileEditorProvider.java
new file mode 100644
index 000000000000..631b5a9eaf4c
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyFileEditorProvider.java
@@ -0,0 +1,64 @@
+package com.jetbrains.python.edu.editor;
+
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorPolicy;
+import com.intellij.openapi.fileEditor.FileEditorProvider;
+import com.intellij.openapi.fileEditor.FileEditorState;
+import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jdom.Element;
+import org.jetbrains.annotations.NotNull;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.TaskFile;
+
+/**
+ * User: lia
+ * Date: 10.05.14
+ * Time: 12:45
+ */
+class StudyFileEditorProvider implements FileEditorProvider, DumbAware {
+ static final private String EDITOR_TYPE_ID = "StudyEditor";
+ final private FileEditorProvider defaultTextEditorProvider = TextEditorProvider.getInstance();
+
+ @Override
+ public boolean accept(@NotNull Project project, @NotNull VirtualFile file) {
+ TaskFile taskFile = StudyTaskManager.getInstance(project).getTaskFile(file);
+ return taskFile != null && !taskFile.isUserCreated();
+ }
+
+ @NotNull
+ @Override
+ public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) {
+ return new StudyEditor(project, file);
+ }
+
+ @Override
+ public void disposeEditor(@NotNull FileEditor editor) {
+ defaultTextEditorProvider.disposeEditor(editor);
+ }
+
+ @NotNull
+ @Override
+ public FileEditorState readState(@NotNull Element sourceElement, @NotNull Project project, @NotNull VirtualFile file) {
+ return defaultTextEditorProvider.readState(sourceElement, project, file);
+ }
+
+ @Override
+ public void writeState(@NotNull FileEditorState state, @NotNull Project project, @NotNull Element targetElement) {
+ defaultTextEditorProvider.writeState(state, project, targetElement);
+ }
+
+ @NotNull
+ @Override
+ public String getEditorTypeId() {
+ return EDITOR_TYPE_ID;
+ }
+
+ @NotNull
+ @Override
+ public FileEditorPolicy getPolicy() {
+ return FileEditorPolicy.HIDE_DEFAULT_EDITOR;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java
new file mode 100644
index 000000000000..abf648c5c82a
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java
@@ -0,0 +1,112 @@
+package com.jetbrains.python.edu.projectView;
+
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.SimpleTextAttributes;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.*;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class StudyDirectoryNode extends PsiDirectoryNode {
+ private final PsiDirectory myValue;
+ private final Project myProject;
+
+ public StudyDirectoryNode(@NotNull final Project project,
+ PsiDirectory value,
+ ViewSettings viewSettings) {
+ super(project, value, viewSettings);
+ myValue = value;
+ myProject = project;
+ }
+
+ @Override
+ protected void updateImpl(PresentationData data) {
+ data.setIcon(StudyIcons.Unchecked);
+ String valueName = myValue.getName();
+ StudyTaskManager studyTaskManager = StudyTaskManager.getInstance(myProject);
+ Course course = studyTaskManager.getCourse();
+ if (course == null) {
+ return;
+ }
+ if (valueName.equals(myProject.getName())) {
+ data.clearText();
+ data.addText(course.getName(), new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, JBColor.BLUE));
+ data.addText(" (" + valueName + ")", SimpleTextAttributes.GRAYED_ATTRIBUTES);
+ return;
+ }
+ if (valueName.contains(Task.TASK_DIR)) {
+ TaskFile file = null;
+ for (PsiElement child : myValue.getChildren()) {
+ VirtualFile virtualFile = child.getContainingFile().getVirtualFile();
+ file = studyTaskManager.getTaskFile(virtualFile);
+ if (file != null) {
+ break;
+ }
+ }
+ if (file != null) {
+ Task task = file.getTask();
+ setStudyAttributes(task, data, task.getName());
+ }
+ }
+ if (valueName.contains(Lesson.LESSON_DIR)) {
+ int lessonIndex = Integer.parseInt(valueName.substring(Lesson.LESSON_DIR.length())) - 1;
+ Lesson lesson = course.getLessons().get(lessonIndex);
+ setStudyAttributes(lesson, data, lesson.getName());
+ }
+
+ if (valueName.contains(Course.PLAYGROUND_DIR)) {
+ if (myValue.getParent() != null) {
+ if (!myValue.getParent().getName().contains(Course.PLAYGROUND_DIR)) {
+ data.setPresentableText(Course.PLAYGROUND_DIR);
+ data.setIcon(StudyIcons.Playground);
+ return;
+ }
+ }
+ }
+ data.setPresentableText(valueName);
+ }
+
+ @Override
+ public int getTypeSortWeight(boolean sortByType) {
+ String name = myValue.getName();
+ if (name.contains(Lesson.LESSON_DIR) || name.contains(Task.TASK_DIR)) {
+ String logicalName = name.contains(Lesson.LESSON_DIR) ? Lesson.LESSON_DIR : Task.TASK_DIR;
+ return StudyUtils.getIndex(name, logicalName) + 1;
+ }
+ return name.contains(Course.PLAYGROUND_DIR) ? 0 : 3;
+ }
+
+ private static void setStudyAttributes(Stateful stateful, PresentationData data, String additionalName) {
+ StudyStatus taskStatus = stateful.getStatus();
+ switch (taskStatus) {
+ case Unchecked: {
+ updatePresentation(data, additionalName, JBColor.blue, StudyIcons.Unchecked);
+ break;
+ }
+ case Solved: {
+ updatePresentation(data, additionalName, new JBColor(new Color(0, 134, 0), new Color(98, 150, 85)), StudyIcons.Checked);
+ break;
+ }
+ case Failed: {
+ updatePresentation(data, additionalName, JBColor.RED, StudyIcons.Failed);
+ }
+ }
+ }
+
+ private static void updatePresentation(PresentationData data, String additionalName, JBColor color, Icon icon) {
+ data.clearText();
+ data.addText(additionalName, new SimpleTextAttributes(Font.PLAIN, color));
+ data.setIcon(icon);
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyTreeStructureProvider.java b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyTreeStructureProvider.java
new file mode 100644
index 000000000000..e301bc3e626b
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyTreeStructureProvider.java
@@ -0,0 +1,83 @@
+package com.jetbrains.python.edu.projectView;
+
+import com.intellij.ide.projectView.TreeStructureProvider;
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.ide.projectView.impl.nodes.PsiFileNode;
+import com.intellij.ide.util.treeView.AbstractTreeNode;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class StudyTreeStructureProvider implements TreeStructureProvider, DumbAware {
+ @NotNull
+ @Override
+ public Collection<AbstractTreeNode> modify(@NotNull AbstractTreeNode parent,
+ @NotNull Collection<AbstractTreeNode> children,
+ ViewSettings settings) {
+ if (!needModify(parent)) {
+ return children;
+ }
+ Collection<AbstractTreeNode> nodes = new ArrayList<AbstractTreeNode>();
+ for (AbstractTreeNode node : children) {
+ Project project = node.getProject();
+ if (project != null) {
+ if (node.getValue() instanceof PsiDirectory) {
+ PsiDirectory nodeValue = (PsiDirectory)node.getValue();
+ if (!nodeValue.getName().contains(Task.USER_TESTS)) {
+ StudyDirectoryNode newNode = new StudyDirectoryNode(project, nodeValue, settings);
+ nodes.add(newNode);
+ }
+ }
+ else {
+ if (parent instanceof StudyDirectoryNode) {
+ if (node instanceof PsiFileNode) {
+ PsiFileNode psiFileNode = (PsiFileNode)node;
+ VirtualFile virtualFile = psiFileNode.getVirtualFile();
+ if (virtualFile == null) {
+ return nodes;
+ }
+ TaskFile taskFile = StudyTaskManager.getInstance(project).getTaskFile(virtualFile);
+ if (taskFile != null) {
+ nodes.add(node);
+ }
+ String parentName = parent.getName();
+ if (parentName != null) {
+ if (parentName.equals(Course.PLAYGROUND_DIR)) {
+ nodes.add(node);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return nodes;
+ }
+
+ private static boolean needModify(AbstractTreeNode parent) {
+ Project project = parent.getProject();
+ if (project != null) {
+ StudyTaskManager studyTaskManager = StudyTaskManager.getInstance(project);
+ if (studyTaskManager.getCourse() == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Object getData(Collection<AbstractTreeNode> selected, String dataName) {
+ return null;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyCondition.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyCondition.java
new file mode 100644
index 000000000000..5add6c934ecd
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyCondition.java
@@ -0,0 +1,25 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Condition;
+import com.jetbrains.python.edu.StudyTaskManager;
+
+/**
+ * author: liana
+ * data: 7/29/14.
+ */
+public class StudyCondition implements Condition, DumbAware {
+ public static boolean VALUE = false;
+ @Override
+ public boolean value(Object o) {
+ if (o instanceof Project) {
+ Project project = (Project) o;
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ if (taskManager.getCourse() != null) {
+ VALUE = true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.form b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.form
new file mode 100644
index 000000000000..133c38d4e8f8
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.form
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.python.edu.ui.StudyNewProjectPanel">
+ <grid id="27dc6" binding="myContentPanel" layout-manager="GridLayoutManager" row-count="2" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints>
+ <xy x="20" y="20" width="500" height="400"/>
+ </constraints>
+ <properties/>
+ <border type="none"/>
+ <children>
+ <grid id="54488" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+ <margin top="0" left="0" bottom="0" right="0"/>
+ <constraints>
+ <grid row="1" column="1" row-span="1" col-span="3" vsize-policy="2" hsize-policy="4" anchor="0" fill="3" indent="0" use-parent-layout="false">
+ <minimum-size width="-1" height="60"/>
+ <preferred-size width="-1" height="60"/>
+ </grid>
+ </constraints>
+ <properties/>
+ <border type="line">
+ <color color="-6709600"/>
+ </border>
+ <children>
+ <component id="213f6" class="javax.swing.JLabel" binding="myAuthorLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value=""/>
+ </properties>
+ </component>
+ <component id="d754d" class="javax.swing.JLabel" binding="myDescriptionLabel">
+ <constraints>
+ <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties>
+ <text value=""/>
+ </properties>
+ </component>
+ </children>
+ </grid>
+ <component id="6c40c" class="javax.swing.JLabel">
+ <constraints>
+ <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
+ <preferred-size width="81" height="-1"/>
+ </grid>
+ </constraints>
+ <properties>
+ <font/>
+ <horizontalTextPosition value="0"/>
+ <text value="Courses:"/>
+ </properties>
+ </component>
+ <component id="21ac6" class="javax.swing.JComboBox" binding="myCoursesComboBox">
+ <constraints>
+ <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="7" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+ </constraints>
+ <properties/>
+ </component>
+ <component id="5c614" class="com.intellij.openapi.ui.FixedSizeButton" binding="myBrowseButton">
+ <constraints>
+ <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
+ <minimum-size width="30" height="25"/>
+ </grid>
+ </constraints>
+ <properties/>
+ </component>
+ <component id="f1e10" class="javax.swing.JButton" binding="myRefreshButton">
+ <constraints>
+ <grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
+ <minimum-size width="30" height="23"/>
+ </grid>
+ </constraints>
+ <properties>
+ <hideActionText value="false"/>
+ <text value=""/>
+ <toolTipText value="Refresh course list"/>
+ <verticalAlignment value="1"/>
+ <verticalTextPosition value="1"/>
+ </properties>
+ </component>
+ </children>
+ </grid>
+</form>
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.java
new file mode 100644
index 000000000000..0f1ec08a8856
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.java
@@ -0,0 +1,196 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.facet.ui.FacetValidatorsManager;
+import com.intellij.facet.ui.ValidationResult;
+import com.intellij.openapi.fileChooser.FileChooser;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.Consumer;
+import com.jetbrains.python.edu.StudyDirectoryProjectGenerator;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.CourseInfo;
+import icons.StudyIcons;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * author: liana
+ * data: 7/31/14.
+ */
+public class StudyNewProjectPanel{
+ private Set<CourseInfo> myAvailableCourses = new HashSet<CourseInfo>();
+ private JComboBox myCoursesComboBox;
+ private JButton myBrowseButton;
+ private JButton myRefreshButton;
+ private JPanel myContentPanel;
+ private JLabel myAuthorLabel;
+ private JLabel myDescriptionLabel;
+ private final StudyDirectoryProjectGenerator myGenerator;
+ private static final String CONNECTION_ERROR = "<html>Failed to download courses.<br>Check your Internet connection.</html>";
+ private static final String INVALID_COURSE = "Selected course is invalid";
+ private FacetValidatorsManager myValidationManager;
+
+ public StudyNewProjectPanel(StudyDirectoryProjectGenerator generator) {
+ myGenerator = generator;
+ Map<CourseInfo, File> courses = myGenerator.getCourses();
+ if (courses.isEmpty()) {
+ setError(CONNECTION_ERROR);
+ }
+ else {
+ myAvailableCourses = courses.keySet();
+ for (CourseInfo courseInfo : myAvailableCourses) {
+ myCoursesComboBox.addItem(courseInfo);
+ }
+ myAuthorLabel.setText("Author: " + StudyUtils.getFirst(myAvailableCourses).getAuthor());
+ myDescriptionLabel.setText(StudyUtils.getFirst(myAvailableCourses).getDescription());
+ //setting the first course in list as selected
+ myGenerator.setSelectedCourse(StudyUtils.getFirst(myAvailableCourses));
+ setOK();
+ }
+ initListeners();
+ myRefreshButton.setVisible(true);
+ myRefreshButton.setIcon(StudyIcons.Refresh);
+ }
+
+ private void initListeners() {
+
+ final FileChooserDescriptor fileChooser = new FileChooserDescriptor(true, false, false, true, false, false) {
+ @Override
+ public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
+ return file.isDirectory() || StudyUtils.isZip(file.getName());
+ }
+
+ @Override
+ public boolean isFileSelectable(VirtualFile file) {
+ return StudyUtils.isZip(file.getName());
+ }
+ };
+ myBrowseButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ FileChooser.chooseFile(fileChooser, null, null,
+ new Consumer<VirtualFile>() {
+ @Override
+ public void consume(VirtualFile file) {
+ String fileName = file.getPath();
+ int oldSize = myAvailableCourses.size();
+ CourseInfo courseInfo = myGenerator.addLocalCourse(fileName);
+ if (courseInfo != null) {
+ if (oldSize != myAvailableCourses.size()) {
+ myCoursesComboBox.addItem(courseInfo);
+ }
+ myCoursesComboBox.setSelectedItem(courseInfo);
+ setOK();
+ }
+ else {
+ setError(INVALID_COURSE);
+ myCoursesComboBox.removeAllItems();
+ myCoursesComboBox.addItem(CourseInfo.INVALID_COURSE);
+ for (CourseInfo course : myAvailableCourses) {
+ myCoursesComboBox.addItem(course);
+ }
+ myCoursesComboBox.setSelectedItem(CourseInfo.INVALID_COURSE);
+ }
+ }
+ });
+ }
+ });
+ myRefreshButton.addActionListener(new RefreshActionListener());
+ myCoursesComboBox.addActionListener(new CourseSelectedListener());
+ }
+
+ private void setError(String errorMessage) {
+ myGenerator.setValidationResult(new ValidationResult(errorMessage));
+ if (myValidationManager != null) {
+ myValidationManager.validate();
+ }
+ }
+
+ private void setOK() {
+ myGenerator.setValidationResult(ValidationResult.OK);
+ if (myValidationManager != null) {
+ myValidationManager.validate();
+ }
+ }
+
+ public JPanel getContentPanel() {
+ return myContentPanel;
+ }
+
+ public void registerValidators(final FacetValidatorsManager manager) {
+ myValidationManager = manager;
+ }
+
+
+ /**
+ * Handles refreshing courses
+ * Old courses added to new courses only if their
+ * meta file still exists in local file system
+ */
+ private class RefreshActionListener implements ActionListener {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ myGenerator.downloadAndUnzip(true);
+ Map<CourseInfo, File> downloadedCourses = myGenerator.loadCourses();
+ if (downloadedCourses.isEmpty()) {
+ setError(CONNECTION_ERROR);
+ return;
+ }
+ Map<CourseInfo, File> oldCourses = myGenerator.getLoadedCourses();
+ Map<CourseInfo, File> newCourses = new HashMap<CourseInfo, File>();
+ for (Map.Entry<CourseInfo, File> course : oldCourses.entrySet()) {
+ File courseFile = course.getValue();
+ if (courseFile.exists()) {
+ newCourses.put(course.getKey(), courseFile);
+ }
+ }
+ for (Map.Entry<CourseInfo, File> course : downloadedCourses.entrySet()) {
+ CourseInfo courseName = course.getKey();
+ if (newCourses.get(courseName) == null) {
+ newCourses.put(courseName, course.getValue());
+ }
+ }
+ myCoursesComboBox.removeAllItems();
+
+ for (CourseInfo courseInfo : newCourses.keySet()) {
+ myCoursesComboBox.addItem(courseInfo);
+ }
+ myGenerator.setSelectedCourse(StudyUtils.getFirst(newCourses.keySet()));
+
+ myGenerator.setCourses(newCourses);
+ myAvailableCourses = newCourses.keySet();
+ myGenerator.flushCache();
+ }
+ }
+
+
+ /**
+ * Handles selecting course in combo box
+ * Sets selected course in combo box as selected in
+ * {@link StudyNewProjectPanel#myGenerator}
+ */
+ private class CourseSelectedListener implements ActionListener {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ JComboBox cb = (JComboBox)e.getSource();
+ CourseInfo selectedCourse = (CourseInfo)cb.getSelectedItem();
+ if (selectedCourse == null || selectedCourse.equals(CourseInfo.INVALID_COURSE)) {
+ myAuthorLabel.setText("");
+ myDescriptionLabel.setText("");
+ return;
+ }
+ myAuthorLabel.setText("Author: " + selectedCourse.getAuthor());
+ myCoursesComboBox.removeItem(CourseInfo.INVALID_COURSE);
+ myDescriptionLabel.setText(selectedCourse.getDescription());
+ myGenerator.setSelectedCourse(selectedCourse);
+ setOK();
+ }
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyProgressBar.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyProgressBar.java
new file mode 100644
index 000000000000..97fa00d0f70d
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyProgressBar.java
@@ -0,0 +1,92 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.ui.GraphicsConfig;
+import com.intellij.ui.ColorUtil;
+import com.intellij.ui.Gray;
+import com.intellij.ui.JBColor;
+import com.intellij.util.ui.GraphicsUtil;
+import com.intellij.util.ui.UIUtil;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+
+public class StudyProgressBar extends JComponent implements DumbAware {
+ public static final Color BLUE = JBColor.BLUE;
+ private static final Color SHADOW1 = new JBColor(Gray._190, JBColor.border());
+ private static final Color SHADOW2 = Gray._105;
+ private static final int BRICK_WIDTH = 10;
+ private static final int BRICK_SPACE = 2;
+ private final int myHeight;
+ private final int myIndent;
+ private double myFraction = 0.0;
+ private Color myColor = BLUE;
+
+ public StudyProgressBar(double fraction, Color color, int height, int indent) {
+ myFraction = fraction;
+ myColor = color;
+ myHeight = height;
+ myIndent = indent;
+ }
+
+ private int getBricksToDraw(double fraction) {
+ int bricksTotal = (getWidth() - 8) / (BRICK_WIDTH + BRICK_SPACE);
+ return (int)(bricksTotal * fraction) + 1;
+ }
+
+ protected void paintComponent(Graphics g) {
+ final GraphicsConfig config = GraphicsUtil.setupAAPainting(g);
+ Graphics2D g2 = (Graphics2D)g;
+ if (myFraction > 1) {
+ myFraction = 1;
+ }
+
+ Dimension size = getSize();
+ double width = size.getWidth() - 2*myIndent;
+ g2.setPaint(UIUtil.getListBackground());
+ Rectangle2D rect = new Rectangle2D.Double(myIndent, 0, width, myHeight);
+ g2.fill(rect);
+
+ g2.setPaint(new JBColor(SHADOW1, JBColor.border()));
+ rect.setRect(myIndent, 0, width, myHeight);
+ int arcWidth = 5;
+ int arcHeight = 5;
+ g2.drawRoundRect(myIndent, 0, (int)width, myHeight, arcWidth, arcHeight);
+ g2.setPaint(SHADOW2);
+ g2.drawRoundRect(myIndent, 0, (int)width, myHeight, arcWidth, arcHeight);
+
+ int y_center = myHeight / 2;
+ int y_steps = myHeight / 2 - 3;
+ int alpha_step = y_steps > 0 ? (255 - 70) / y_steps : 255 - 70;
+ int x_offset = 4;
+
+ g.setClip(4 + myIndent, 3, (int)width - 6, myHeight - 4);
+
+ int bricksToDraw = myFraction == 0 ? 0 : getBricksToDraw(myFraction);
+ for (int i = 0; i < bricksToDraw; i++) {
+ g2.setPaint(myColor);
+ UIUtil.drawLine(g2, x_offset, y_center, x_offset + BRICK_WIDTH - 1, y_center);
+ for (int j = 0; j < y_steps; j++) {
+ Color color = ColorUtil.toAlpha(myColor, 255 - alpha_step * (j + 1));
+ g2.setPaint(color);
+ UIUtil.drawLine(g2, x_offset, y_center - 1 - j, x_offset + BRICK_WIDTH - 1, y_center - 1 - j);
+ if (!(y_center % 2 != 0 && j == y_steps - 1)) {
+ UIUtil.drawLine(g2, x_offset, y_center + 1 + j, x_offset + BRICK_WIDTH - 1, y_center + 1 + j);
+ }
+ }
+ g2.setColor(
+ ColorUtil.toAlpha(myColor, 255 - alpha_step * (y_steps / 2 + 1)));
+ g2.drawRect(x_offset, y_center - y_steps, BRICK_WIDTH - 1, myHeight - 7);
+ x_offset += BRICK_WIDTH + BRICK_SPACE;
+ }
+ config.restore();
+ }
+
+ @Override
+ public Dimension getMaximumSize() {
+ Dimension dimension = super.getMaximumSize();
+ dimension.height = myHeight + 10;
+ return dimension;
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyTestContentPanel.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyTestContentPanel.java
new file mode 100644
index 000000000000..dee4fbaf9d01
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyTestContentPanel.java
@@ -0,0 +1,67 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.ui.DocumentAdapter;
+import com.intellij.ui.components.JBScrollPane;
+import org.jetbrains.annotations.NotNull;
+import com.jetbrains.python.edu.course.UserTest;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.text.BadLocationException;
+import java.awt.*;
+
+public class StudyTestContentPanel extends JPanel {
+ public static final Dimension PREFERRED_SIZE = new Dimension(300, 200);
+ private static final Font HEADER_FONT = new Font("Arial", Font.BOLD, 16);
+ private final JTextArea myInputArea = new JTextArea();
+ private final JTextArea myOutputArea = new JTextArea();
+ public StudyTestContentPanel(UserTest userTest) {
+ this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+ initContentLabel("input", myInputArea);
+ myInputArea.getDocument().addDocumentListener(new BufferUpdater(userTest.getInputBuffer()));
+ myOutputArea.getDocument().addDocumentListener(new BufferUpdater(userTest.getOutputBuffer()));
+ initContentLabel("output", myOutputArea);
+ setEditable(userTest.isEditable());
+ }
+
+ private void initContentLabel(final String headerText, @NotNull final JTextArea contentArea) {
+ JLabel headerLabel = new JLabel(headerText);
+ headerLabel.setFont(HEADER_FONT);
+ this.add(headerLabel);
+ this.add(new JSeparator(SwingConstants.HORIZONTAL));
+ JScrollPane scroll = new JBScrollPane(contentArea);
+ scroll.setPreferredSize(PREFERRED_SIZE);
+ this.add(scroll);
+ }
+
+ private void setEditable(boolean isEditable) {
+ myInputArea.setEditable(isEditable);
+ myOutputArea.setEditable(isEditable);
+ }
+ public void addInputContent(final String content) {
+ myInputArea.setText(content);
+ }
+
+ public void addOutputContent(final String content) {
+ myOutputArea.setText(content);
+ }
+
+ private class BufferUpdater extends DocumentAdapter {
+ private final StringBuilder myBuffer;
+
+ private BufferUpdater(StringBuilder buffer) {
+ myBuffer = buffer;
+ }
+
+ @Override
+ protected void textChanged(DocumentEvent e) {
+ myBuffer.delete(0, myBuffer.length());
+ try {
+ myBuffer.append(e.getDocument().getText(0, e.getDocument().getLength()));
+ }
+ catch (BadLocationException e1) {
+ e1.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java
new file mode 100644
index 000000000000..a553978c416a
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java
@@ -0,0 +1,81 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowFactory;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.content.Content;
+import com.intellij.ui.content.ContentFactory;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.Lesson;
+import com.jetbrains.python.edu.course.LessonInfo;
+import com.jetbrains.python.edu.course.StudyStatus;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.List;
+
+public class StudyToolWindowFactory implements ToolWindowFactory, DumbAware {
+ public static final String STUDY_TOOL_WINDOW = "Course Description";
+ JPanel contentPanel = new JPanel();
+
+ @Override
+ public void createToolWindowContent(@NotNull final Project project, @NotNull final ToolWindow toolWindow) {
+ if (StudyTaskManager.getInstance(project).getCourse() != null) {
+ contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.PAGE_AXIS));
+ contentPanel.add(Box.createRigidArea(new Dimension(10, 0)));
+ StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+ Course course = taskManager.getCourse();
+ if (course == null) {
+ return;
+ }
+ String courseName = UIUtil.toHtml("<h1>" + course.getName() + "</h1>", 10);
+ String description = UIUtil.toHtml(course.getDescription(), 5);
+ String author = taskManager.getCourse().getAuthor();
+ String authorLabel = UIUtil.toHtml("<b>Author: </b>" + author, 5);
+ contentPanel.add(new JLabel(courseName));
+ contentPanel.add(new JLabel(authorLabel));
+ contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
+ contentPanel.add(new JLabel(description));
+
+ int taskNum = 0;
+ int taskSolved = 0;
+ int lessonsCompleted = 0;
+ List<Lesson> lessons = course.getLessons();
+ for (Lesson lesson : lessons) {
+ if (lesson.getStatus() == StudyStatus.Solved) {
+ lessonsCompleted++;
+ }
+ LessonInfo lessonInfo = lesson.getLessonInfo();
+ taskNum += lessonInfo.getTaskNum();
+ taskSolved += lessonInfo.getTaskSolved();
+ }
+ String completedLessons = String.format("%d of %d lessons completed", lessonsCompleted, course.getLessons().size());
+ String completedTasks = String.format("%d of %d tasks completed", taskSolved, taskNum);
+ String tasksLeft = String.format("%d of %d tasks left", taskNum - taskSolved, taskNum);
+ contentPanel.add(Box.createVerticalStrut(10));
+ addStatistics(completedLessons);
+ addStatistics(completedTasks);
+
+ double percent = (taskSolved * 100.0) / taskNum;
+ contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
+ StudyProgressBar studyProgressBar = new StudyProgressBar(percent / 100, JBColor.GREEN, 40, 10);
+ contentPanel.add(studyProgressBar);
+ addStatistics(tasksLeft);
+ ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
+ Content content = contentFactory.createContent(contentPanel, "", true);
+ toolWindow.getContentManager().addContent(content);
+ }
+ }
+
+ private void addStatistics(String statistics) {
+ String labelText = UIUtil.toHtml(statistics, 5);
+ contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
+ JLabel statisticLabel = new JLabel(labelText);
+ contentPanel.add(statisticLabel);
+ }
+}
diff --git a/python/edu/learn-python/testData/course.json b/python/edu/learn-python/testData/course.json
new file mode 100644
index 000000000000..bff570afd29a
--- /dev/null
+++ b/python/edu/learn-python/testData/course.json
@@ -0,0 +1,130 @@
+{
+ "name": "Python для начинающих",
+ "description": "Начальный курс по языку Python",
+ "lessons": [
+ {
+ "name": "Первые программа",
+ "task_list": [
+ {
+ "name": "Задание 1",
+ "text": "hello-text.html",
+ "test_file": "hello-tests.py",
+ "test_num": 1,
+ "task_files": {
+ "helloworld.py": {
+ "task_windows": [
+ {
+ "line": 0,
+ "start": 0,
+ "text": "type operator",
+ "hint": "hello-text.html",
+ "possible_answer": "print"
+ },
+ {
+ "line": 0,
+ "start": 33,
+ "text": "type your name",
+ "hint": "empty_study.docs",
+ "possible_answer": "Liana"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "Задание 2",
+ "text": "matchends-text.html",
+ "test_file": "matchends-test.py",
+ "test_num": 3,
+ "task_files": {
+ "match_ends.py": {
+ "task_windows": [
+ {
+ "line": 1,
+ "start": 43,
+ "text": "condition",
+ "hint": "empty_study.docs",
+ "possible_answer": ">="
+ },
+ {
+ "line": 1,
+ "start": 61,
+ "text": "index",
+ "hint": "empty_study.docs",
+ "possible_answer": "0"
+ },
+ {
+ "line": 1,
+ "start": 73,
+ "text": "index",
+ "hint": "empty_study.docs",
+ "possible_answer": "-1"
+ },
+ {
+ "line": 2,
+ "start": 11,
+ "text": "function",
+ "hint": "list.docs",
+ "possible_answer": "len"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "Простые задачи",
+ "task_list": [
+ {
+ "name": "Задание 1",
+ "text": "sum-text.html",
+ "test_file": "sum_tests.py",
+ "test_num": 3,
+ "user_tests": [
+ {
+ "input": "sum-input.txt",
+ "output": "sum-output"
+ }
+ ],
+ "task_files": {
+ "sum.py": {
+ "task_windows": [
+ {
+ "line": 4,
+ "start": 15,
+ "text": "получите из консоли имя файла",
+ "hint": "argv.docs",
+ "possible_answer": "sys.argv[1]"
+ },
+ {
+ "line": 5,
+ "start": 8,
+ "text": "откройте файл на запись",
+ "hint": "empty_study.docs",
+ "possible_answer": "open(filename, 'r')"
+ },
+ {
+ "line": 10,
+ "start": 4,
+ "text": "закройте файл",
+ "hint": "empty_study.docs",
+ "possible_answer": "f.close()"
+ },
+ {
+ "line": 11,
+ "start": 14,
+ "text": "правильно проинициализируйте значение",
+ "hint": "empty_study.docs",
+ "possible_answer": "-sys.maxint"
+ }
+ ]
+ }
+ }
+ }
+
+ ]
+
+ }
+ ]
+} \ No newline at end of file
diff --git a/python/edu/learn-python/tests/JsonParserTest.java b/python/edu/learn-python/tests/JsonParserTest.java
new file mode 100644
index 000000000000..903f0a539acf
--- /dev/null
+++ b/python/edu/learn-python/tests/JsonParserTest.java
@@ -0,0 +1,37 @@
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.Course;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * author: liana
+ * data: 7/4/14.
+ */
+public class JsonParserTest {
+ private Course myCourse = null;
+ @Before
+ public void setUp() throws FileNotFoundException {
+ Reader reader = new InputStreamReader(new FileInputStream("EDIDE/testData/course.json"));
+ Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+ myCourse = gson.fromJson(reader, Course.class);
+ }
+
+ @Test
+ public void testCourseLevel() {
+ assertEquals(myCourse.getName(), "Python для начинающих");
+ assertEquals(StudyUtils.getFirst(myCourse.getLessons().get(1).getTaskList().get(0).getUserTests()).getInput(), "sum-input.txt");
+ assertEquals(myCourse.getLessons().size(), 2);
+ assertEquals(myCourse.getLessons().get(0).getTaskList().size(), 2);
+ assertEquals(myCourse.getLessons().get(1).getTaskList().size(), 1);
+ }
+}