18 |
19 | true
20 | true
21 | false
22 | false
23 |
24 |
25 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | CodeGRITS is developed by a team of HCI and SE researchers at the University of Notre Dame and Vanderbilt University.
2 |
3 | Ningzhi Tang (Equal Contribution)
4 | Junwen An (Equal Contribution)
5 | Meng Chen
6 | Aakash Bansa
7 | Yu Huang
8 | Collin McMillan
9 | Toby Jia-Jun Li
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 SaNDwich Lab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CodeGRITS
2 |
3 | [](https://codegrits.github.io/CodeGRITS/) [](https://codegrits.github.io/CodeGRITS/docs/index.html) [](https://codegrits.github.io/CodeGRITS/static/paper.pdf) [](https://www.youtube.com/watch?v=d-YsJfW2NMI) [](https://archive.softwareheritage.org/swh:1:dir:32d91426f07dd0f4b36ba05bc708b5a25ad06dd3) [](https://github.com/codegrits/CodeGRITS/blob/main/LICENSE)
4 |
5 | [CodeGRITS](https://codegrits.github.io/CodeGRITS/) stands for **G**aze **R**ecording & **I**DE **T**racking **S**ystem. It's a plugin developed by the [SaNDwich Lab](https://toby.li/) and is specially designed for empirical software engineering researchers. CodeGRITS is built on top of [IntelliJ Platform SDK](https://plugins.jetbrains.com/docs/intellij/welcome.html), with wide compatibility with the entire family of [JetBrains IDEs](https://www.jetbrains.com/) and [Tobii eye-tracking devices](https://www.tobii.com/), to track developers’ IDE interactions and eye gaze data.
6 |
7 |
This class is used to check the availability of the python environment and the eye-tracking device, and to get the eye tracker name and the available frequencies.
73 |
74 |
75 |
--------------------------------------------------------------------------------
/site/faq.md:
--------------------------------------------------------------------------------
1 | ---
2 | label: FAQ
3 | icon: question
4 | order: 0
5 | ---
6 |
7 | # Frequently Asked Questions (FAQ)
8 |
9 | #### Q1. What is the difference between CodeGRITS and iTrace?
10 |
11 | [iTrace](https://www.i-trace.org/) is a similar tool to CodeGRITS for collecting developers' eye gaze data in several
12 | IDEs, including Eclipse, Visual Studio, and Atom. However, CodeGRITS is built for JetBrains IDEs, which have increased
13 | popularity in the industry and academia.
14 |
15 | CodeGRITS also provides a set of extra functionalities, notably IDE tracking and screen recording, for empirical SE
16 | researchers. See [Trackers](usage.md#trackers) for more details.
17 |
18 | #### Q2. Can I use CodeGRITS without an eye-tracking device?
19 |
20 | Yes. CodeGRITS provides mouse simulation as a substitute for eye gaze. You could also uncheck the `Eye Tracking` option
21 | in the configuration window to disable the eye tracker to only use the IDE tracker and screen recorder.
22 |
23 | #### Q3. How to integrate other eye-tracking devices with CodeGRITS?
24 |
25 | See [Accommodating New Eye Trackers](developer.md#accommodating-new-eye-trackers).
26 |
27 | #### Q4. How to use CodeGRITS in other JetBrains IDEs?
28 |
29 | See [Accommodating New IDEs](developer.md#accommodating-new-ides).
30 |
31 | #### Q5. How efficient is the processing of raw eye gaze data?
32 |
33 | In CodeGRITS, the efficiency of the processing of gaze data is negligible. For each gaze, we calculate the average time
34 | from the timestamp in the raw data to the timestamp after all processing is complete. This processing primarily involves
35 | location mapping and upward traversal of the AST. The average delay is 4.32 ms, equating to delays of approximately
36 | 12.98% for 30Hz, 25.96% for 60Hz, and 51.92% for 120Hz eye gaze data. With such
37 | a high sampling frequency, meaningful changes in the content of the code editor's page are extremely rare within this
38 | short time frame, which ensures the accuracy of gaze processing. Moreover, compared
39 | to [iTrace](https://www.i-trace.org/), our method of receiving data from the
40 | eye-tracking device is more efficient, ensuring that all sampled gazes are mapped without any loss.
41 |
42 | #### Q6. How much storage space does CodeGRITS require?
43 |
44 | Generally speaking, a high sample frequency generates a large amount of gaze data. To conserve storage space, we
45 | only perform upward traversal of the AST of the first gaze and record the hierarchy structure, and mark the rest
46 | as `same`. This approach significantly reduces storage space. In a previous debugging study, we set the eye-tracking
47 | device's sample frequency to 60Hz, and during the 20-minute experiment, the eye-tracking data amounted to only about
48 | 40MB.
49 |
50 | #### Q7. Which eye gazes can be analyzed, and can gazes on the UI be understood?
51 |
52 | In CodeGRITS, we only analyze gazes within the code editor. For example, in the following figure, only gazes within the
53 | red rectangle are mapped to the source code tokens and performed the upward traversal of the AST. For gazes outside the
54 | code editor, such as those on the file explorer, menubar, tool window, console, etc., we only record their raw
55 | information and add a remark `Fail | Out of Text Editor`. CodeGRITS cannot understand the semantics of gazes on the UI.
56 |
57 |
58 |
59 |
60 |
61 | However, if certain UI elements within the editor are triggered, such as the list for auto-completion or the inline
62 | definition display, CodeGRITS mistakenly interprets the gaze as being directed at the code content below the UI (when it
63 | should actually be on the UI). We have not yet found an appropriate method to resolve this issue, which may lead to
64 | inaccuracies in some gaze analyses.
--------------------------------------------------------------------------------
/site/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/favicon.png
--------------------------------------------------------------------------------
/site/google3c26aad8347fc661.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google3c26aad8347fc661.html
--------------------------------------------------------------------------------
/site/license.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | ---
4 |
5 | MIT License
6 |
7 | Copyright (c) 2023 SaNDwich Lab
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining a copy
10 | of this software and associated documentation files (the "Software"), to deal
11 | in the Software without restriction, including without limitation the rights
12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | copies of the Software, and to permit persons to whom the Software is
14 | furnished to do so, subject to the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be included in all
17 | copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | SOFTWARE.
--------------------------------------------------------------------------------
/site/rename.py:
--------------------------------------------------------------------------------
1 | """
2 | Rename all the `ClassName.html` to `classname.html` in the /docs folder.
3 | This is to make sure that the links in the JavaDoc is useful after the
4 | processing by Retype.
5 | """
6 | import os
7 |
8 |
9 | def get_candidate_list(path):
10 | candidate_list = []
11 | for root, dirs, files in os.walk(path):
12 | for file in files:
13 | if file.endswith('.java'):
14 | candidate_list.append(file.replace('.java', '.html'))
15 | return candidate_list
16 |
17 |
18 | def rename_text(file_path, candidate_list):
19 | with open(file_path, 'r') as f:
20 | content = f.read()
21 | for candidate in candidate_list:
22 | content = content.replace(candidate, candidate.lower())
23 | with open(file_path, 'w') as f:
24 | f.write(content)
25 |
26 |
27 | if __name__ == '__main__':
28 | path_java = '../src/main/java'
29 | path_docs = './docs'
30 | candidates = get_candidate_list(path_java)
31 | for root, dirs, files in os.walk(path_docs):
32 | for file in files:
33 | if file.endswith('.html'):
34 | rename_text(os.path.join(root, file), candidates)
35 | if file in candidates:
36 | os.rename(os.path.join(root, file), os.path.join(root, file.lower()))
37 |
--------------------------------------------------------------------------------
/site/static/add-label.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/add-label.png
--------------------------------------------------------------------------------
/site/static/ast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/ast.png
--------------------------------------------------------------------------------
/site/static/config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/config.png
--------------------------------------------------------------------------------
/site/static/eye-data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/eye-data.png
--------------------------------------------------------------------------------
/site/static/eye-tracker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/eye-tracker.png
--------------------------------------------------------------------------------
/site/static/ide-data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/ide-data.png
--------------------------------------------------------------------------------
/site/static/ide-tracker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/ide-tracker.png
--------------------------------------------------------------------------------
/site/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/logo.png
--------------------------------------------------------------------------------
/site/static/overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/overview.png
--------------------------------------------------------------------------------
/site/static/paper.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/paper.pdf
--------------------------------------------------------------------------------
/site/static/range.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/range.png
--------------------------------------------------------------------------------
/site/static/toolbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/toolbar.png
--------------------------------------------------------------------------------
/site/static/visualization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codegrits/CodeGRITS/dda784fc9a31693f3ca0df900443ade7da5baadc/site/static/visualization.png
--------------------------------------------------------------------------------
/site/welcome.md:
--------------------------------------------------------------------------------
1 | ---
2 | label: Welcome
3 | icon: home
4 | order: 100
5 | ---
6 |
7 | # Welcome to CodeGRITS
8 |
9 | !!! :zap: NEWS! :zap:
10 | We would present CodeGRITS at
11 | [ICSE 2024 Demo Track](https://conf.researchr.org/track/icse-2024/icse-2024-demonstrations?#program).
12 | Welcome to join us and discuss with us about it!
13 | !!!
14 |
15 | CodeGRITS stands for **G**aze **R**ecording & **I**DE **T**racking **S**ystem. It's a plugin developed by
16 | the [SaNDwich Lab](https://toby.li/) and is specially designed for empirical software engineering researchers.
17 | CodeGRITS is built on top
18 | of [IntelliJ Platform SDK](https://plugins.jetbrains.com/docs/intellij/welcome.html), with wide compatibility with the
19 | entire family of JetBrains IDEs and [Tobii eye-tracking devices](https://www.tobii.com/),
20 | to track developers’ IDE interactions and eye gaze data. The source code is available
21 | on [GitHub](https://github.com/codegrits/CodeGRITS) with [Javadoc](docs/index.html) documentation.
22 |
23 |
24 |
25 |
26 |
27 | The data collected by CodeGRITS can be used by empirical SE researchers to understand the behaviors of developers,
28 | especially those related to eye gaze. CodeGRITS also provides a [real-time data API](developer.md)
29 | for future plugin developers and researchers to design context-aware programming support tools.
30 |
31 | !!!
32 | CodeGRITS is still in its developmental stage as a research tool. Our goal is to make it mature enough and beneficial
33 | for the community, particularly for those involved in empirical software engineering and eye tracking research. We
34 | encourage the community to contribute through [GitHub Issue](https://github.com/codegrits/CodeGRITS/issues) for any
35 | suggestions or issues, aiding in its improvement.
36 |
37 | For any inquiries, please email us at ntang@nd.edu or jan2@nd.edu. If you're interested in
38 | using CodeGRITS in your research, don't hesitate to email us for setup support. We are delighted to provide
39 | tailored assistance based on your specific OS and JetBrains IDE environment.
40 | !!!
41 |
42 | ### Cross-platform and Multilingual Support
43 |
44 | - [x] CodeGRITS provides cross-platform support for Windows, macOS,
45 | and Linux, and is expected to be compatible with the entire family of JetBrains IDEs, including IntelliJ IDEA,
46 | PyCharm, WebStorm, etc.
47 | - [x] CodeGRITS could extract the abstract syntax tree (AST) structure of eye gazes on multiple
48 | programming languages, as long as the IDE supports them, including Java, Python, C/C++, JavaScript, etc.
49 |
50 | !!!warning 🚨 macOS Support 🚨
51 | CodeGRITS has been primarily developed and tested on Windows and Linux, with only partial testing on macOS.
52 | There may be some unnoticed bugs on macOS. We have created a
53 | [`mac` branch](https://github.com/codegrits/CodeGRITS/tree/mac) for this and have fixed some known issues.
54 | If you encounter additional issues on macOS, please feel free to report them to us.
55 | !!!
56 |
57 | ## Key Features
58 |
59 | - :mag: **IDE Tracking**: CodeGRITS tracks developers’ IDE interactions, including mouse clicks, keyboard inputs, etc.
60 | - :eye: **Eye Tracking**: CodeGRITS tracks developers’ eye gaze data
61 | from [Tobii eye-tracking devices](https://www.tobii.com/), and maps them to corresponding source code elements.
62 | - :computer: **Screen Recording**: CodeGRITS simultaneously records developers’ screen for visualizing their behaviors.
63 | - 🔨 **Research Toolkit**: CodeGRITS provides a set of extra features for empirical SE
64 | researchers, including dynamic configuration, activity labeling, real-time data API, etc.
65 | - 🗃️ **Data Export**: CodeGRITS exports data in XML format for further data analysis. See [Data Format](data.md)
66 | for more details.
67 |
68 |
69 |
70 |
71 |
72 | ## Citation
73 |
74 | The paper of CodeGRITS has been accepted
75 | by [ICSE 2024 Demonstrations Track](https://conf.researchr.org/track/icse-2024/icse-2024-demonstrations).
76 | The PDF version is available [here](static/paper.pdf).
77 | Please cite the following if you use CodeGRITS in your research.
78 |
79 | ```bibtex
80 | @inproceedings{tang2024codegrits,
81 | title={CodeGRITS: A Research Toolkit for Developer Behavior and Eye Tracking in IDE},
82 | author={Tang, Ningzhi and An, Junwen and Chen, Meng and Bansal, Aakash and Huang, Yu and McMillan, Collin and Li, Toby Jia-Jun},
83 | booktitle={46th International Conference on Software Engineering Companion (ICSE-Companion '24)},
84 | year={2024},
85 | organization={ACM}
86 | }
87 | ```
--------------------------------------------------------------------------------
/src/main/java/actions/AddLabelAction.java:
--------------------------------------------------------------------------------
1 | package actions;
2 |
3 | import com.intellij.notification.Notification;
4 | import com.intellij.notification.NotificationType;
5 | import com.intellij.openapi.actionSystem.AnAction;
6 | import com.intellij.openapi.actionSystem.AnActionEvent;
7 | import org.jetbrains.annotations.NotNull;
8 |
9 | /**
10 | * This class is the action for adding labels.
11 | */
12 | public class AddLabelAction extends AnAction {
13 |
14 | private String description;
15 | private static boolean isEnabled = false;
16 |
17 | @Override
18 | public void update(@NotNull AnActionEvent e) {
19 | e.getPresentation().setText(description);
20 | e.getPresentation().setEnabled(isEnabled);
21 | }
22 |
23 | /**
24 | * This method is called when the action is performed. It will show a notification to indicate that the label is successfully added.
25 | *
26 | * @param e The action event.
27 | */
28 | @Override
29 | public void actionPerformed(@NotNull AnActionEvent e) {
30 | Notification notification = new Notification("CodeGRITS Notification Group", "Add label",
31 | "Successfully add label \"" + description + "\"!", NotificationType.INFORMATION);
32 | notification.notify(e.getProject());
33 | }
34 |
35 | public void setDescription(String description) {
36 | this.description = description;
37 | }
38 |
39 | public static void setIsEnabled(boolean isEnabled) {
40 | AddLabelAction.isEnabled = isEnabled;
41 | }
42 |
43 | @Override
44 | public @NotNull String getTemplateText() {
45 | return description;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/actions/AddLabelActionGroup.java:
--------------------------------------------------------------------------------
1 | package actions;
2 |
3 | import com.intellij.openapi.actionSystem.*;
4 | import entity.Config;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.util.List;
8 |
9 | /**
10 | * This class is the action group for adding labels.
11 | */
12 | public class AddLabelActionGroup extends DefaultActionGroup {
13 |
14 | private static boolean isEnabled = true;
15 | private boolean defaultLabelsLoaded = false;
16 |
17 | /**
18 | * This method is called when the action is performed. It will register all the {@link AddLabelAction}s from the configuration file.
19 | *
20 | * @param e The action event.
21 | */
22 | @Override
23 | public void update(@NotNull AnActionEvent e) {
24 | Config config = new Config();
25 | if (!defaultLabelsLoaded && config.configExists()) {
26 | config.loadFromJson();
27 | ActionManager actionManager = ActionManager.getInstance();
28 | DefaultActionGroup actionGroup = (DefaultActionGroup) actionManager.getAction("CodeGRITS.AddLabelActionGroup");
29 | actionGroup.removeAll();
30 | List labels = config.getLabels();
31 | for (String label : labels) {
32 | AddLabelAction addLabelAction = new AddLabelAction();
33 | addLabelAction.setDescription(label);
34 | actionManager.registerAction("CodeGRITS.AddLabel.[" + label + "]", addLabelAction);
35 | actionGroup.add(addLabelAction);
36 | }
37 | defaultLabelsLoaded = true;
38 | }
39 | }
40 |
41 | public static void setIsEnabled(boolean isEnabled) {
42 | AddLabelActionGroup.isEnabled = isEnabled;
43 | }
44 | }
--------------------------------------------------------------------------------
/src/main/java/actions/ConfigAction.java:
--------------------------------------------------------------------------------
1 | package actions;
2 |
3 | import com.intellij.openapi.actionSystem.AnAction;
4 | import com.intellij.openapi.actionSystem.AnActionEvent;
5 | import com.intellij.openapi.project.Project;
6 | import components.ConfigDialog;
7 | import org.jetbrains.annotations.NotNull;
8 |
9 | import java.io.IOException;
10 |
11 | /**
12 | * This class is the action for configuring the application.
13 | */
14 | public class ConfigAction extends AnAction {
15 |
16 | private static boolean isEnabled = true;
17 |
18 | @Override
19 | public void update(@NotNull AnActionEvent e) {
20 | e.getPresentation().setEnabled(isEnabled);
21 | }
22 |
23 | /**
24 | * This method is called when the action is performed. It will show the configuration dialog.
25 | *
26 | * @param e The action event.
27 | */
28 | @Override
29 | public void actionPerformed(@NotNull AnActionEvent e) {
30 | Project project = e.getProject();
31 | ConfigDialog configDialog;
32 | try {
33 | configDialog = new ConfigDialog(project);
34 | } catch (IOException | InterruptedException ex) {
35 | throw new RuntimeException(ex);
36 | }
37 | configDialog.show();
38 | }
39 |
40 | public static void setIsEnabled(boolean isEnabled) {
41 | ConfigAction.isEnabled = isEnabled;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/actions/PauseResumeTrackingAction.java:
--------------------------------------------------------------------------------
1 | package actions;
2 |
3 | import com.intellij.openapi.actionSystem.AnAction;
4 | import com.intellij.openapi.actionSystem.AnActionEvent;
5 | import org.jetbrains.annotations.NotNull;
6 | import trackers.ScreenRecorder;
7 |
8 | import java.io.IOException;
9 |
10 | /**
11 | * This class is the action for pausing/resuming tracking.
12 | */
13 | public class PauseResumeTrackingAction extends AnAction {
14 | private final ScreenRecorder screenRecorder = ScreenRecorder.getInstance();
15 |
16 | /**
17 | * Update the text of the action button. If the tracking is not started, the button is disabled.
18 | *
19 | * @param e The action event.
20 | */
21 | @Override
22 | public void update(@NotNull AnActionEvent e) {
23 | if (StartStopTrackingAction.isTracking()) {
24 | e.getPresentation().setEnabled(true);
25 | if (StartStopTrackingAction.isPaused()) {
26 | e.getPresentation().setText("Resume Tracking");
27 | } else {
28 | e.getPresentation().setText("Pause Tracking");
29 | }
30 | } else {
31 | e.getPresentation().setText("Pause Tracking");
32 | e.getPresentation().setEnabled(false);
33 | }
34 | }
35 |
36 | /**
37 | * This method is called when the action is performed. It will pause/resume tracking.
38 | *
39 | * @param e The action event.
40 | */
41 | @Override
42 | public void actionPerformed(@NotNull AnActionEvent e) {
43 | if (StartStopTrackingAction.isPaused()) {
44 | screenRecorder.resumeRecording();
45 | StartStopTrackingAction.resumeTracking();
46 | AddLabelAction.setIsEnabled(true);
47 | ConfigAction.setIsEnabled(false);
48 |
49 | } else {
50 | StartStopTrackingAction.pauseTracking();
51 | ConfigAction.setIsEnabled(false);
52 | AddLabelAction.setIsEnabled(false);
53 | try {
54 | screenRecorder.pauseRecording();
55 | } catch (IOException ex) {
56 | throw new RuntimeException(ex);
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/java/actions/StartStopTrackingAction.java:
--------------------------------------------------------------------------------
1 | package actions;
2 |
3 | import com.intellij.notification.Notification;
4 | import com.intellij.notification.NotificationType;
5 | import com.intellij.openapi.actionSystem.AnAction;
6 | import com.intellij.openapi.actionSystem.AnActionEvent;
7 | import components.ConfigDialog;
8 | import entity.Config;
9 | import org.jetbrains.annotations.NotNull;
10 | import trackers.EyeTracker;
11 | import trackers.IDETracker;
12 | import trackers.ScreenRecorder;
13 | import utils.AvailabilityChecker;
14 |
15 | import javax.swing.*;
16 | import javax.xml.parsers.ParserConfigurationException;
17 | import javax.xml.transform.TransformerException;
18 | import java.io.IOException;
19 | import java.util.Objects;
20 |
21 | /**
22 | * This class is the action for starting/stopping tracking.
23 | */
24 | public class StartStopTrackingAction extends AnAction {
25 |
26 | /**
27 | * This variable indicates whether the tracking is started.
28 | */
29 | private static boolean isTracking = false;
30 | /**
31 | * This variable is the IDE tracker.
32 | */
33 | private static IDETracker iDETracker;
34 | /**
35 | * This variable is the eye tracker.
36 | */
37 | private static EyeTracker eyeTracker;
38 | /**
39 | * This variable is the screen recorder.
40 | */
41 | private final ScreenRecorder screenRecorder = ScreenRecorder.getInstance();
42 | /**
43 | * This variable is the configuration.
44 | */
45 | Config config = new Config();
46 |
47 | /**
48 | * Update the text of the action button.
49 | *
50 | * @param e The action event.
51 | */
52 | @Override
53 | public void update(@NotNull AnActionEvent e) {
54 | e.getPresentation().setText(isTracking ? "Stop Tracking" : "Start Tracking");
55 | }
56 |
57 | /**
58 | * This method is called when the action is performed. It will start/stop tracking.
59 | *
60 | * @param e The action event.
61 | */
62 | @Override
63 | public void actionPerformed(@NotNull AnActionEvent e) {
64 | if (config.configExists()) {
65 | config.loadFromJson();
66 | } else {
67 | Notification notification = new Notification("CodeGRITS Notification Group", "Configuration",
68 | "Please configure the plugin first.", NotificationType.WARNING);
69 | notification.notify(e.getProject());
70 | return;
71 | }
72 | try {
73 | if (!isTracking) {
74 | if (config.getCheckBoxes().get(1)) {
75 | if (!AvailabilityChecker.checkPythonEnvironment(config.getPythonInterpreter())) {
76 | JOptionPane.showMessageDialog(null, "Python interpreter not found. Please configure the plugin first.");
77 | return;
78 | }
79 | if (config.getEyeTrackerDevice() != 0 && !AvailabilityChecker.checkEyeTracker(config.getPythonInterpreter())) {
80 | JOptionPane.showMessageDialog(null, "Eye tracker not found. Please configure the mouse simulation first.");
81 | return;
82 | }
83 | }
84 |
85 | isTracking = true;
86 | ConfigAction.setIsEnabled(false);
87 | AddLabelActionGroup.setIsEnabled(true);
88 | String projectPath = e.getProject() != null ? e.getProject().getBasePath() : "";
89 | String realDataOutputPath = Objects.equals(config.getDataOutputPath(), ConfigDialog.selectDataOutputPlaceHolder)
90 | ? projectPath : config.getDataOutputPath();
91 | realDataOutputPath += "/" + System.currentTimeMillis() + "/";
92 |
93 | if (config.getCheckBoxes().get(2)) {
94 | screenRecorder.setDataOutputPath(realDataOutputPath);
95 | screenRecorder.startRecording();
96 | }
97 |
98 | iDETracker = IDETracker.getInstance();
99 | iDETracker.setProjectPath(projectPath);
100 | iDETracker.setDataOutputPath(realDataOutputPath);
101 | iDETracker.startTracking(e.getProject());
102 |
103 | if (config.getCheckBoxes().get(1)) {
104 | eyeTracker = new EyeTracker();
105 | eyeTracker.setProjectPath(projectPath);
106 | eyeTracker.setDataOutputPath(realDataOutputPath);
107 | eyeTracker.setPythonInterpreter(config.getPythonInterpreter());
108 | eyeTracker.setSampleFrequency(config.getSampleFreq());
109 | eyeTracker.setDeviceIndex(config.getEyeTrackerDevice());
110 | eyeTracker.setPythonScriptTobii();
111 | eyeTracker.setPythonScriptMouse();
112 | eyeTracker.startTracking(e.getProject());
113 | }
114 | AddLabelAction.setIsEnabled(true);
115 |
116 | } else {
117 | isTracking = false;
118 | iDETracker.stopTracking();
119 | AddLabelAction.setIsEnabled(false);
120 | ConfigAction.setIsEnabled(true);
121 | if (config.getCheckBoxes().get(1) && eyeTracker != null) {
122 | eyeTracker.stopTracking();
123 | }
124 | if (config.getCheckBoxes().get(2)) {
125 | screenRecorder.stopRecording();
126 | }
127 | eyeTracker = null;
128 | }
129 | } catch (ParserConfigurationException | TransformerException | IOException | InterruptedException ex) {
130 | throw new RuntimeException(ex);
131 | }
132 | }
133 |
134 | public static boolean isTracking() {
135 | return isTracking;
136 | }
137 |
138 | public static boolean isPaused() {
139 | return !iDETracker.isTracking();
140 | }
141 |
142 | public static void pauseTracking() {
143 | iDETracker.pauseTracking();
144 | if (eyeTracker != null) {
145 | eyeTracker.pauseTracking();
146 | }
147 | }
148 |
149 | public static void resumeTracking() {
150 | iDETracker.resumeTracking();
151 | if (eyeTracker != null) {
152 | eyeTracker.resumeTracking();
153 | }
154 | }
155 |
156 | }
--------------------------------------------------------------------------------
/src/main/java/api/RealtimeDataImpl.java:
--------------------------------------------------------------------------------
1 | package api;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import trackers.EyeTracker;
5 | import trackers.IDETracker;
6 |
7 | import javax.xml.parsers.ParserConfigurationException;
8 | import javax.xml.transform.TransformerException;
9 | import java.io.BufferedReader;
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.io.InputStreamReader;
13 | import java.net.Socket;
14 | import java.util.function.Consumer;
15 |
16 | /**
17 | * This class provides the API for getting real-time data from the IDE and eye tracker.
18 | */
19 | public class RealtimeDataImpl {
20 |
21 | // make it singleton
22 | private static RealtimeDataImpl realtimeData = new RealtimeDataImpl();
23 | Socket socket;
24 | InputStream dataInputStream;
25 | private Consumer ideTrackerDataHandler;
26 | private Consumer eyeTrackerDataHandler;
27 | private static IDETracker ideTracker;
28 | private static EyeTracker eyeTracker;
29 |
30 | private RealtimeDataImpl() {
31 | }
32 |
33 | public static RealtimeDataImpl getInstance() {
34 | return realtimeData;
35 | }
36 |
37 | public void checkEnvironment() {
38 | System.out.println("Hello World!");
39 | }
40 |
41 | public void getRawIDETrackerData(Project project) throws ParserConfigurationException {
42 | ideTracker = IDETracker.getInstance();
43 | ideTracker.startTracking(project);
44 | }
45 |
46 | public void getRawEyeTrackerData() {
47 |
48 | }
49 |
50 | public void stopIDETrackerData() throws TransformerException {
51 | ideTracker.stopTracking();
52 | }
53 |
54 | public void stopEyeTrackerData() {
55 |
56 | }
57 |
58 | public void getHandledIDETrackerData(Project project) throws ParserConfigurationException, IOException {
59 | if (ideTrackerDataHandler == null) {
60 | return;
61 | }
62 | ideTracker = IDETracker.getInstance();
63 | ideTracker.setIsRealTimeDataTransmitting(true);
64 | ideTracker.startTracking(project);
65 | Thread ideTrackerThread = new Thread(() -> {
66 | try {
67 |
68 | Thread.sleep(2000);
69 | Socket socket = new Socket("localhost", 12346);
70 | InputStream dataInputStream = socket.getInputStream();
71 | InputStreamReader inputStreamReader = new InputStreamReader(dataInputStream);
72 | BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
73 | while (true) {
74 | String line = bufferedReader.readLine();
75 | System.out.println(ideTrackerDataHandler.toString());
76 | // ideTrackerDataHandler.accept(line);
77 | System.out.println(line);
78 | }
79 | } catch (IOException | InterruptedException e) {
80 | throw new RuntimeException(e);
81 | }
82 | });
83 | ideTrackerThread.start();
84 | }
85 |
86 | public void getHandledEyeTrackerData() {
87 | if (eyeTrackerDataHandler == null) {
88 | throw new RuntimeException("Eye Tracker Data Handler not set!");
89 | }
90 | }
91 |
92 | public void setIDETrackerDataHandler(Consumer ideTrackerDataHandler) {
93 | this.ideTrackerDataHandler = ideTrackerDataHandler;
94 | }
95 |
96 | public void setEyeTrackerDataHandler(Consumer eyeTrackerDataHandler) {
97 | this.eyeTrackerDataHandler = eyeTrackerDataHandler;
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/java/components/AlertDialog.java:
--------------------------------------------------------------------------------
1 | package components;
2 |
3 | import com.intellij.openapi.ui.DialogWrapper;
4 | import org.jetbrains.annotations.NotNull;
5 | import org.jetbrains.annotations.Nullable;
6 |
7 | import javax.swing.*;
8 | import java.awt.*;
9 |
10 | /**
11 | * This class is used to create a dialog to show alert message.
12 | */
13 | public class AlertDialog extends DialogWrapper {
14 | /**
15 | * The label of the alert message.
16 | */
17 | private final String label;
18 | /**
19 | * The icon of the alert message.
20 | */
21 | private final Icon icon;
22 |
23 | /**
24 | * The constructor of the class.
25 | *
26 | * @param label The alert message.
27 | * @param icon The icon of the alert message.
28 | */
29 | public AlertDialog(String label, Icon icon) {
30 |
31 | super(true); // use current window as parent
32 | this.label = label;
33 | this.icon = icon;
34 | init();
35 | setTitle("Alert");
36 |
37 | }
38 |
39 | /**
40 | * Create the OK button.
41 | *
42 | * @return The OK button.
43 | */
44 | @Override
45 | protected Action @NotNull [] createActions() {
46 | return new Action[]{getOKAction()};
47 | }
48 |
49 | /**
50 | * Create the center panel of the dialog.
51 | *
52 | * @return The center panel of the dialog.
53 | */
54 | @Override
55 | protected @Nullable JComponent createCenterPanel() {
56 | JPanel dialogPanel = new JPanel();
57 | dialogPanel.setLayout(new BoxLayout(dialogPanel, BoxLayout.Y_AXIS));
58 |
59 | JLabel label = new JLabel(this.label);
60 | JLabel icon = new JLabel();
61 | label.setIcon(this.icon);
62 |
63 | icon.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 5));
64 | label.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
65 | label.setAlignmentX(Component.CENTER_ALIGNMENT);
66 | icon.setAlignmentX(Component.CENTER_ALIGNMENT);
67 | dialogPanel.add(label);
68 |
69 | return dialogPanel;
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/entity/Config.java:
--------------------------------------------------------------------------------
1 | package entity;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.JsonElement;
5 | import com.google.gson.JsonObject;
6 | import com.google.gson.JsonParser;
7 | import com.google.gson.reflect.TypeToken;
8 | import com.intellij.openapi.application.PathManager;
9 |
10 | import java.io.FileReader;
11 | import java.io.FileWriter;
12 | import java.io.Serializable;
13 | import java.util.List;
14 |
15 | /**
16 | * This class is used to store the configuration of the application.
17 | */
18 | public class Config implements Serializable {
19 | private List checkBoxes;
20 | private List labels;
21 | private Double sampleFreq;
22 | private String pythonInterpreter;
23 | private String dataOutputPath;
24 | private Integer eyeTrackerDevice;
25 |
26 | /**
27 | * The constructor of the Config class.
28 | *
29 | * @param checkBoxes The list of the checkboxes.
30 | * @param labels The list of the labels.
31 | * @param sampleFreq The sample frequency.
32 | * @param pythonInterpreter The path of the python interpreter.
33 | * @param dataOutputPath The path of the data output folder.
34 | * @param eyeTrackerDevice The index of the eye tracker device.
35 | */
36 | public Config(List checkBoxes, List labels, Double sampleFreq, String pythonInterpreter, String dataOutputPath, Integer eyeTrackerDevice) {
37 | this.checkBoxes = checkBoxes;
38 | this.labels = labels;
39 | this.sampleFreq = sampleFreq;
40 | this.pythonInterpreter = pythonInterpreter;
41 | this.dataOutputPath = dataOutputPath;
42 | this.eyeTrackerDevice = eyeTrackerDevice;
43 | }
44 |
45 | /**
46 | * The constructor of the Config class.
47 | */
48 | public Config() {
49 | }
50 |
51 | public boolean configExists() {
52 | try (FileReader fileReader = new FileReader(PathManager.getPluginsPath() + "/config.json")) {
53 | return true;
54 | } catch (Exception e) {
55 | return false;
56 | }
57 | }
58 |
59 | /**
60 | * Save the configuration as a JSON file.
61 | */
62 | public void saveAsJson() {
63 | JsonObject jsonObject = new JsonObject();
64 | if (sampleFreq == null) sampleFreq = 30.0;
65 | jsonObject.addProperty("pythonInterpreter", pythonInterpreter);
66 | jsonObject.addProperty("sampleFreq", sampleFreq);
67 | jsonObject.addProperty("labels", labels.toString());
68 | jsonObject.addProperty("checkBoxes", checkBoxes.toString());
69 | jsonObject.addProperty("dataOutputPath", dataOutputPath);
70 | jsonObject.addProperty("eyeTrackerDevice", eyeTrackerDevice);
71 |
72 | Gson gson = new Gson();
73 | try (FileWriter fileWriter = new FileWriter(PathManager.getPluginsPath() + "/config.json")) {
74 | System.out.println(PathManager.getPluginsPath() + "/config.json");
75 | fileWriter.write(gson.toJson(jsonObject));
76 | } catch (Exception e) {
77 | throw new RuntimeException(e);
78 | }
79 | }
80 |
81 | /**
82 | * Load the configuration from the JSON file.
83 | */
84 | public void loadFromJson() {
85 | try (FileReader fileReader = new FileReader(PathManager.getPluginsPath() + "/config.json")) {
86 | Gson gson = new Gson();
87 | JsonElement jsonElement = JsonParser.parseReader(fileReader);
88 | JsonObject jsonObject = jsonElement.getAsJsonObject();
89 | pythonInterpreter = jsonObject.get("pythonInterpreter").getAsString();
90 | sampleFreq = jsonObject.get("sampleFreq").getAsDouble();
91 | dataOutputPath = jsonObject.get("dataOutputPath").getAsString();
92 | eyeTrackerDevice = jsonObject.get("eyeTrackerDevice").getAsInt();
93 | String labelsString = jsonObject.get("labels").getAsString().substring(1, jsonObject.get("labels").getAsString().length() - 1);
94 | if (labelsString.equals("")) {
95 | labels = List.of();
96 | } else labels = List.of(labelsString.split(", "));
97 | checkBoxes = gson.fromJson(jsonObject.get("checkBoxes").getAsString(), new TypeToken>() {
98 | }.getType());
99 | } catch (Exception e) {
100 | throw new RuntimeException(e);
101 | }
102 | }
103 |
104 | public String getPythonInterpreter() {
105 | return pythonInterpreter;
106 | }
107 |
108 | public Double getSampleFreq() {
109 | return sampleFreq;
110 | }
111 |
112 | public List getCheckBoxes() {
113 | return checkBoxes;
114 | }
115 |
116 | public List getLabels() {
117 | return labels;
118 | }
119 |
120 | public String getDataOutputPath() {
121 | return dataOutputPath;
122 | }
123 |
124 | public Integer getEyeTrackerDevice() {
125 | return eyeTrackerDevice;
126 | }
127 |
128 | public String toString() {
129 | return "Config{" +
130 | "checkBoxes=" + checkBoxes +
131 | ", labels=" + labels +
132 | ", sampleFreq=" + sampleFreq +
133 | ", pythonInterpreter='" + pythonInterpreter + '\'' +
134 | ", dataOutputPath='" + dataOutputPath + '\'' +
135 | ", eyeTrackerDevice=" + eyeTrackerDevice +
136 | '}';
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/src/main/java/trackers/ScreenRecorder.java:
--------------------------------------------------------------------------------
1 | package trackers;
2 |
3 | import com.opencsv.CSVWriter;
4 | import org.bytedeco.ffmpeg.global.avcodec;
5 | import org.bytedeco.javacv.*;
6 | import org.bytedeco.javacv.Frame;
7 |
8 | import java.awt.*;
9 | import java.io.File;
10 | import java.io.FileWriter;
11 | import java.io.IOException;
12 | import java.util.ArrayList;
13 | import java.util.Timer;
14 | import java.util.TimerTask;
15 |
16 | /**
17 | * This class is the screen recorder.
18 | */
19 | public class ScreenRecorder {
20 |
21 | /**
22 | * This variable indicates the state of the screen recorder. 0: initial state; only startAction enabled 1: started, not paused; stopAction and pauseAction enabled 2: started, paused; only resumeAction enabled
23 | */
24 | int state = 0;
25 | /**
26 | * This variable indicates the frame rate of the screen recorder.
27 | */
28 | int frameRate = 4; // higher frame rate (e.g., 12) will result in larger file size and blurry video
29 | private FrameRecorder recorder;
30 | private FrameGrabber grabber;
31 | private final ArrayList timeList = new ArrayList<>();
32 | private CSVWriter csvWriter;
33 | boolean isRecording = false;
34 | /**
35 | * This variable indicates the current clip number.
36 | */
37 | private int clipNumber = 1;
38 | /**
39 | * This variable indicates the current frame number.
40 | */
41 | private int frameNumber = 0;
42 | private String dataOutputPath = "";
43 | private static ScreenRecorder instance = null;
44 |
45 | public static ScreenRecorder getInstance() {
46 | if (instance == null) {
47 | instance = new ScreenRecorder();
48 | }
49 | return instance;
50 | }
51 |
52 | /**
53 | * Create the encoder using {@link FFmpegFrameGrabber} and {@link FFmpegFrameRecorder}.
54 | */
55 | private void createEncoder() throws IOException {
56 | // avfoundation for macOS, gdigrab for Windows, xcbgrab for Linux
57 | if (utils.OSDetector.isMac()) {
58 | grabber = new FFmpegFrameGrabber("1");
59 | grabber.setFormat("avfoundation");
60 | } else if (utils.OSDetector.isWindows()) {
61 | grabber = new FFmpegFrameGrabber("desktop");
62 | grabber.setFormat("gdigrab");
63 | } else if (utils.OSDetector.isUnix()) {
64 | grabber = new FFmpegFrameGrabber(":0.0");
65 | grabber.setFormat("x11grab");
66 | } else {
67 | throw new IOException("Unsupported OS");
68 | }
69 | grabber.setFrameRate(frameRate);
70 | GraphicsConfiguration config = GraphicsEnvironment.getLocalGraphicsEnvironment().
71 | getDefaultScreenDevice().getDefaultConfiguration();
72 | // set image width and height to be the same as the resolution of the *first* screen (in case of multiple screens)
73 | grabber.setImageWidth((int) (Toolkit.getDefaultToolkit().getScreenSize().width * config.getDefaultTransform().getScaleX()));
74 | grabber.setImageHeight((int) (Toolkit.getDefaultToolkit().getScreenSize().height * config.getDefaultTransform().getScaleY()));
75 | grabber.setOption("offset_x", "0");
76 | grabber.setOption("offset_y", "0");
77 | grabber.start();
78 |
79 | recorder = FrameRecorder.createDefault(dataOutputPath + "/screen_recording/clip_" + clipNumber + ".mp4", grabber.getImageWidth(), grabber.getImageHeight());
80 | recorder.setFrameRate(frameRate);
81 | recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
82 | recorder.start();
83 | }
84 |
85 | /**
86 | * Start recording the screen. Reset the clip number and invoke {@link #recordScreen()}.
87 | */
88 | public void startRecording() throws IOException {
89 | state = 1;
90 | clipNumber = 1;
91 | timeList.clear();
92 | isRecording = true;
93 | File file = new File(dataOutputPath + "/screen_recording/frames.csv");
94 | file.getParentFile().mkdirs();
95 | csvWriter = new CSVWriter(new FileWriter(file));
96 | csvWriter.writeNext(new String[]{"timestamp", "frame_number", "clip_number"});
97 | timeList.add(new String[]{String.valueOf(System.currentTimeMillis()), "Start", String.valueOf(clipNumber)});
98 | try {
99 | recordScreen();
100 | } catch (AWTException | IOException e) {
101 | e.printStackTrace();
102 | }
103 | }
104 |
105 | /**
106 | * Stop recording the screen. Write the time list to the CSV file.
107 | */
108 | public void stopRecording() throws IOException {
109 | state = 0;
110 | isRecording = false;
111 | timeList.add(new String[]{String.valueOf(System.currentTimeMillis()), "Stop", String.valueOf(clipNumber)});
112 | csvWriter.writeAll(timeList);
113 | csvWriter.close();
114 | }
115 |
116 | /**
117 | * Pause recording the screen. Write the time list to the CSV file and increment the clip number.
118 | */
119 | public void pauseRecording() throws IOException {
120 | state = 2;
121 | isRecording = false;
122 | timeList.add(new String[]{String.valueOf(System.currentTimeMillis()), "Pause", String.valueOf(clipNumber)});
123 | clipNumber++;
124 | }
125 |
126 | /**
127 | * Resume recording the screen. Invoke {@link #recordScreen()}.
128 | */
129 | public void resumeRecording() {
130 | state = 1;
131 | isRecording = true;
132 | timeList.add(new String[]{String.valueOf(System.currentTimeMillis()), "Resume", String.valueOf(clipNumber)});
133 | try {
134 | recordScreen();
135 | } catch (AWTException | IOException e) {
136 | e.printStackTrace();
137 | }
138 | }
139 |
140 | /**
141 | * Record the screen. Use {@link Timer} to schedule the recording with the given frame rate.
142 | */
143 | private void recordScreen() throws AWTException, IOException {
144 | createEncoder();
145 | frameNumber = 0;
146 | Timer timer = new Timer();
147 | timer.scheduleAtFixedRate(new TimerTask() {
148 | @Override
149 | public void run() {
150 | if (!isRecording) {
151 | try {
152 | grabber.stop();
153 | recorder.stop();
154 | grabber.release();
155 | recorder.release();
156 | } catch (IOException e) {
157 | throw new RuntimeException(e);
158 | }
159 | timer.cancel();
160 | } else {
161 | frameNumber++;
162 | timeList.add(new String[]{String.valueOf(System.currentTimeMillis()), String.valueOf(frameNumber), String.valueOf(clipNumber)});
163 | try {
164 | Frame frame = grabber.grabFrame();
165 | recorder.record(frame);
166 | } catch (FrameGrabber.Exception | FrameRecorder.Exception e) {
167 | throw new RuntimeException(e);
168 | }
169 |
170 | }
171 | }
172 | }, 0, 1000 / frameRate);
173 | }
174 |
175 | /**
176 | * Set the data output path.
177 | *
178 | * @param dataOutputPath The data output path.
179 | */
180 | public void setDataOutputPath(String dataOutputPath) {
181 | this.dataOutputPath = dataOutputPath;
182 | }
183 | }
--------------------------------------------------------------------------------
/src/main/java/utils/AvailabilityChecker.java:
--------------------------------------------------------------------------------
1 | package utils;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.io.InputStreamReader;
7 | import java.util.List;
8 |
9 | /**
10 | * This class is used to check the availability of the python environment and the eye-tracking device, and to get the eye tracker name and the available frequencies.
11 | */
12 | public class AvailabilityChecker {
13 |
14 | /**
15 | * Check the availability of the python environment, i.e., whether the required python packages are installed.
16 | *
17 | * @param pythonInterpreter The path of the python interpreter.
18 | * @return {@code true} if the python environment is available, {@code false} otherwise.
19 | */
20 | public static boolean checkPythonEnvironment(String pythonInterpreter) throws IOException, InterruptedException {
21 | String pythonScript = """
22 | import tobii_research as tr
23 | from screeninfo import get_monitors
24 | import pyautogui
25 | import time
26 | import sys
27 | import math
28 |
29 | print('OK')
30 | """;
31 |
32 | String line = runPythonScript(pythonInterpreter, pythonScript);
33 | return line.equals("OK");
34 | }
35 |
36 | /**
37 | * Check the availability of the eye-tracking device.
38 | *
39 | * @param pythonInterpreter The path of the python interpreter.
40 | * @return {@code true} if the eye-tracking device is available, {@code false} otherwise.
41 | */
42 | public static boolean checkEyeTracker(String pythonInterpreter) throws IOException, InterruptedException {
43 | String pythonScript = """
44 | import tobii_research as tr
45 |
46 | found_eyetrackers = tr.find_all_eyetrackers()
47 | if found_eyetrackers == ():
48 | print('Not Found')
49 | else:
50 | print('Found')
51 | """;
52 |
53 | String line = runPythonScript(pythonInterpreter, pythonScript);
54 | return line.equals("Found");
55 | }
56 |
57 | /**
58 | * Get the name of the eye-tracking device.
59 | *
60 | * @param pythonInterpreter The path of the python interpreter.
61 | * @return The name of the eye tracker.
62 | */
63 | public static String getEyeTrackerName(String pythonInterpreter) throws IOException, InterruptedException {
64 | String pythonScript = """
65 | import tobii_research as tr
66 |
67 | found_eyetrackers = tr.find_all_eyetrackers()
68 | if found_eyetrackers == ():
69 | print('Not Found')
70 | else:
71 | print(found_eyetrackers[0].device_name)
72 | """;
73 |
74 | return runPythonScript(pythonInterpreter, pythonScript);
75 | }
76 |
77 | /**
78 | * Get the available frequencies of the eye-tracking device.
79 | *
80 | * @param pythonInterpreter The path of the python interpreter.
81 | * @return The available frequencies of the eye tracker.
82 | */
83 | public static List getFrequencies(String pythonInterpreter) throws IOException, InterruptedException {
84 | String pythonScript = """
85 | import tobii_research as tr
86 |
87 | found_eyetrackers = tr.find_all_eyetrackers()
88 | if found_eyetrackers == ():
89 | print('Not Found')
90 | else:
91 | print(found_eyetrackers[0].get_all_gaze_output_frequencies())
92 | """;
93 | String resultTuple = runPythonScript(pythonInterpreter, pythonScript); //(30.0, 60.0, 90.0)
94 |
95 | return List.of(resultTuple.substring(1, resultTuple.length() - 1).split(", "));
96 | }
97 |
98 | /**
99 | * Run a python script with {@code ProcessBuilder} and use {@code BufferedReader} to get the first line of the output.
100 | *
101 | * @param pythonInterpreter The path of the python interpreter.
102 | * @param pythonScript The python script to run.
103 | * @return The first line of the output.
104 | */
105 | private static String runPythonScript(String pythonInterpreter, String pythonScript) throws IOException, InterruptedException {
106 | ProcessBuilder pb = new ProcessBuilder(pythonInterpreter, "-c", pythonScript);
107 | pb.redirectErrorStream(true);
108 | Process p;
109 | p = pb.start();
110 |
111 | InputStream stdout = p.getInputStream();
112 | BufferedReader reader = new BufferedReader(new InputStreamReader(stdout));
113 | String line = reader.readLine();
114 | p.waitFor();
115 | return line;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/utils/OSDetector.java:
--------------------------------------------------------------------------------
1 | package utils;
2 |
3 | /**
4 | * This class is used to detect the operating system.
5 | */
6 | public class OSDetector {
7 | /**
8 | * The operating system name.
9 | */
10 | private static final String OS = System.getProperty("os.name").toLowerCase();
11 |
12 | /**
13 | * Check if the operating system is Windows.
14 | *
15 | * @return {@code true} if the operating system is Windows, {@code false} otherwise.
16 | */
17 | public static boolean isWindows() {
18 | return (OS.contains("win"));
19 | }
20 |
21 | /**
22 | * Check if the operating system is Mac.
23 | *
24 | * @return {@code true} if the operating system is Mac, {@code false} otherwise.
25 | */
26 | public static boolean isMac() {
27 | return (OS.contains("mac"));
28 | }
29 |
30 | /**
31 | * Check if the operating system is Unix.
32 | *
33 | * @return {@code true} if the operating system is Unix, {@code false} otherwise.
34 | */
35 | public static boolean isUnix() {
36 | return (OS.contains("nix") || OS.contains("nux") || OS.contains("aix"));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/utils/RelativePathGetter.java:
--------------------------------------------------------------------------------
1 | package utils;
2 |
3 | /**
4 | * This class is used to get the relative path of a file compared to the project path.
5 | */
6 | public class RelativePathGetter {
7 | /**
8 | * Get the relative path of a file compared to the project path. If the project path is not a prefix of the file path, the absolute path of the file is returned.
9 | *
10 | * @param absolutePath The absolute path of the file.
11 | * @param projectPath The absolute path of the project.
12 | * @return The relative path of the file compared to the project path.
13 | */
14 | public static String getRelativePath(String absolutePath, String projectPath) {
15 | if (absolutePath.length() > projectPath.length() && absolutePath.startsWith(projectPath)) {
16 | return absolutePath.substring(projectPath.length());
17 | }
18 | return absolutePath;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/utils/XMLWriter.java:
--------------------------------------------------------------------------------
1 | package utils;
2 |
3 | import org.w3c.dom.Document;
4 |
5 | import javax.xml.transform.OutputKeys;
6 | import javax.xml.transform.Transformer;
7 | import javax.xml.transform.TransformerException;
8 | import javax.xml.transform.TransformerFactory;
9 | import javax.xml.transform.dom.DOMSource;
10 | import javax.xml.transform.stream.StreamResult;
11 | import java.io.File;
12 |
13 | /**
14 | * This class is used to write the XML document to the XML file.
15 | */
16 | public class XMLWriter {
17 | /**
18 | * Write the formatted XML document to the XML file.
19 | *
20 | * @param document The XML document.
21 | * @param filePath The path of the XML file.
22 | */
23 | public static void writeToXML(Document document, String filePath) throws TransformerException {
24 | TransformerFactory transformerFactory = TransformerFactory.newInstance();
25 | Transformer transformer = transformerFactory.newTransformer();
26 | transformer.setOutputProperty(OutputKeys.INDENT, "yes");
27 | DOMSource source = new DOMSource(document);
28 | StreamResult result = new StreamResult(new File(filePath));
29 | transformer.transform(source, result);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | com.nd.codegrits
5 |
6 |
8 | CodeGRITS
9 |
10 |
11 | SaNDwich Lab
12 |
13 |
16 | Gaze Recording & IDE
18 | Tracking System, which is a plugin specifically designed for SE researchers.
19 | CodeGRITS is built on top of IntelliJ’s SDK, with wide compatibility with the entire family of JetBrains IDEs to
20 | track developers’ IDE interactions and eye gaze data. For more information, please visit our website
21 | CodeGRITS.
22 | ]]>
23 |
24 |
26 | com.intellij.modules.platform
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
52 |
53 |
54 |
56 |
57 |
58 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------