summaryrefslogtreecommitdiff
path: root/python/edu/learn-python/src/com/jetbrains/python/edu/course
diff options
context:
space:
mode:
Diffstat (limited to 'python/edu/learn-python/src/com/jetbrains/python/edu/course')
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/Course.java104
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/CourseInfo.java52
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/Lesson.java109
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/LessonInfo.java60
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/Stateful.java6
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/StudyStatus.java8
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/Task.java201
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskFile.java228
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskWindow.java177
-rw-r--r--python/edu/learn-python/src/com/jetbrains/python/edu/course/UserTest.java41
10 files changed, 986 insertions, 0 deletions
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;
+ }
+}