diff options
Diffstat (limited to 'python/edu/learn-python')
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 Binary files differnew file mode 100644 index 000000000000..f3b24f24b1e2 --- /dev/null +++ b/python/edu/learn-python/resources/courses/introduction_course.zip 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 Binary files differnew file mode 100644 index 000000000000..27a6e362cded --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/Run.png 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 Binary files differnew file mode 100644 index 000000000000..4992191eb9e7 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/WatchInput.png 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 Binary files differnew file mode 100644 index 000000000000..9494f2d0c72e --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/add.png 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 Binary files differnew file mode 100644 index 000000000000..4105a01f1353 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/checked.png 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 Binary files differnew file mode 100644 index 000000000000..e2aaa556056e --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/failed.png 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 Binary files differnew file mode 100644 index 000000000000..3a9716e4e1fb --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/icon.jpg 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 Binary files differnew file mode 100644 index 000000000000..dd1a5d9aebf3 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/next.png 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 Binary files differnew file mode 100644 index 000000000000..d12a751c0c40 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/playground.png 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 Binary files differnew file mode 100644 index 000000000000..0656f81eee83 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/prev.png 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 Binary files differnew file mode 100644 index 000000000000..d595f6b42f56 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh.png 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 Binary files differnew file mode 100644 index 000000000000..218f075d0c18 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh24.png 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 Binary files differnew file mode 100644 index 000000000000..7ef960bcf244 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve.png 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 Binary files differnew file mode 100644 index 000000000000..99aaa1d20a32 --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve_dark.png 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 Binary files differnew file mode 100644 index 000000000000..f10fd560464a --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/showHint.png 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 Binary files differnew file mode 100644 index 000000000000..2145982cf2be --- /dev/null +++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/unchecked.png 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); + } +} |