"
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 2"
- } )
- add( new FormComponent( "javax.swing.JLabel" ) {
- name: "label5"
- "text": "Grading config"
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 3,alignx label,growx 0"
- } )
- add( new FormComponent( "com.intellij.openapi.ui.TextFieldWithBrowseButton" ) {
- name: "gradingConfigPathInput"
- "editable": false
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 3"
- } )
- add( new FormComponent( "javax.swing.JLabel" ) {
- name: "label6"
- "text": "Autograder config"
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 4,alignx label,growx 0"
- } )
- add( new FormComponent( "com.intellij.openapi.ui.TextFieldWithBrowseButton" ) {
- name: "autograderConfigPathInput"
- "editable": false
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 4"
- } )
- add( new FormComponent( "javax.swing.JSeparator" ) {
- name: "separator1"
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 5 2 1"
- } )
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.BorderLayout ) ) {
- name: "generalPanel"
- "border": new javax.swing.border.CompoundBorder( new javax.swing.border.TitledBorder( new javax.swing.border.LineBorder( sfield java.awt.Color darkGray, 1, true ), "i18nKey=AssesmentViewContent.generalPanel.border" ), &EmptyBorder0 new javax.swing.border.EmptyBorder( 5, 5, 5, 5 ) )
- "foreground": sfield java.awt.Color blue
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "btnGradingRound1"
- "text": new FormMessage( null, "AssesmentViewContent.btnGradingRound1.text" )
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class java.lang.String ) {
- "value": "Center"
- } )
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "btnGradingRound2"
- "text": new FormMessage( null, "AssesmentViewContent.btnGradingRound2.text" )
- }, new FormLayoutConstraints( class java.lang.String ) {
- "value": "North"
- } )
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "button5"
- "text": new FormMessage( null, "AssesmentViewContent.button5.text" )
- }, new FormLayoutConstraints( class java.lang.String ) {
- "value": "South"
- } )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 6 2 1,growx"
- } )
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.GridBagLayout ) {
- "$columnSpecs": "0:1.0, 0:1.0"
- "$rowSpecs": "0, 0"
- "$hGap": 5
- "$vGap": 5
- "$alignLeft": true
- "$alignTop": true
- } ) {
- name: "assessmentPanel"
- "border": new javax.swing.border.CompoundBorder( new javax.swing.border.TitledBorder( new javax.swing.border.LineBorder( sfield java.awt.Color darkGray, 1, true ), "i18nKey=AssesmentViewContent.assessmentPanel.border" ), #EmptyBorder0 )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "btnSaveAssessment"
- "text": new FormMessage( null, "AssesmentViewContent.btnSaveAssessment.text" )
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) )
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "submitAssesmentBtn"
- "text": new FormMessage( null, "AssesmentViewContent.submitAssesmentBtn.text" )
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) {
- "gridx": 1
- } )
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "button3"
- "text": new FormMessage( null, "AssesmentViewContent.button3.text" )
- }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) {
- "gridy": 1
- } )
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "button4"
- "text": new FormMessage( null, "AssesmentViewContent.button4.text" )
- }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) {
- "gridx": 1
- "gridy": 1
- } )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 7 2 1"
- } )
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class com.intellij.uiDesigner.core.GridLayoutManager ) {
- "$columnCount": 2
- "$rowCount": 2
- "hGap": 0
- "vGap": 0
- } ) {
- name: "panel5"
- "border": new javax.swing.border.TitledBorder( new javax.swing.border.LineBorder( sfield java.awt.Color darkGray, 1, true ), "i18nKey=AssesmentViewContent.panel5.border" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "label8"
- "text": new FormMessage( null, "AssesmentViewContent.label8.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class com.intellij.uiDesigner.core.GridConstraints ) )
- add( new FormComponent( "edu.kit.kastel.extensions.guis.StatisticsContainer" ) {
- name: "statisticsContainer"
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class com.intellij.uiDesigner.core.GridConstraints ) {
- "column": 1
- } )
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "label9"
- "text": new FormMessage( null, "AssesmentViewContent.label9.text" )
- }, new FormLayoutConstraints( class com.intellij.uiDesigner.core.GridConstraints ) {
- "row": 1
- } )
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "assessmentModeLabel"
- "text": "❌"
- "icon": sfield com.jformdesigner.model.FormObject NULL_VALUE
- "horizontalAlignment": 2
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class com.intellij.uiDesigner.core.GridConstraints ) {
- "column": 1
- "row": 1
- } )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "pad 0,cell 0 8 2 1,growx"
- } )
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.GridBagLayout ) {
- "$columnSpecs": "80, 0:1.0"
- "$rowSpecs": "0, 0"
- "$hGap": 5
- "$vGap": 5
- "$alignLeft": true
- "$alignTop": true
- } ) {
- name: "panel3"
- "border": new javax.swing.border.TitledBorder( new javax.swing.border.LineBorder( sfield java.awt.Color darkGray, 1, false ), "i18nKey=AssesmentViewContent.panel3.border" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- add( new FormComponent( "javax.swing.JLabel" ) {
- name: "label7"
- "text": new FormMessage( null, "AssesmentViewContent.label7.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) {
- "hAlign": 4
- } )
- add( new FormComponent( "com.intellij.openapi.ui.ComboBox" ) {
- name: "backlogSelector"
- }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) {
- "gridx": 1
- } )
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.FlowLayout ) ) {
- name: "panel4"
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "button6"
- "text": new FormMessage( null, "AssesmentViewContent.button6.text" )
- } )
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "button7"
- "text": new FormMessage( null, "AssesmentViewContent.button7.text" )
- } )
- }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) {
- "gridy": 1
- "gridwidth": 2
- } )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 9 2 1"
- } )
- } )
- }, new FormLayoutConstraints( null ) {
- "title": new FormMessage( null, "AssesmentViewContent.assessmentPanel.border" )
- } )
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
- "$layoutConstraints": "fillx,hidemode 3,align left top"
- "$columnConstraints": "[fill]"
- "$rowConstraints": "[grow]"
- } ) {
- name: "GradingPanel"
- add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
- name: "scrollPane"
- "border": new javax.swing.border.EmptyBorder( 0, 0, 0, 0 )
- auxiliary() {
- "JavaCodeGenerator.postCreateCode": "${field}.getVerticalScrollBar().setUnitIncrement(16);"
- }
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class javax.swing.BoxLayout ) {
- "axis": 1
- } ) {
- name: "ratingGroupContainer"
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- } )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 0,growy"
- } )
- }, new FormLayoutConstraints( null ) {
- "title": new FormMessage( null, "AssesmentViewContent.GradingPanel.tab.title" )
- } )
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
- "$layoutConstraints": "fillx,hidemode 3,align left top"
- "$columnConstraints": "[fill][fill]"
- "$rowConstraints": "[36][grow]"
- } ) {
- name: "TestResultsPanel"
- add( new FormComponent( "javax.swing.JLabel" ) {
- name: "label4"
- "text": new FormMessage( null, "AssesmentViewContent.label4.text" )
- "font": new com.jformdesigner.model.SwingDerivedFont( null, 1, 0, false )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 0 2 1"
- } )
- add( new FormContainer( "com.intellij.ui.components.JBScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
- name: "scrollPane1"
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- add( new FormComponent( "com.intellij.ui.table.JBTable" ) {
- name: "testResultsTable"
- } )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 1 2 1,growy"
- } )
- }, new FormLayoutConstraints( null ) {
- "title": new FormMessage( null, "AssesmentViewContent.TestResultsPanel.tab.title" )
- } )
- } )
- }, new FormLayoutConstraints( null ) {
- "location": new java.awt.Point( 0, 0 )
- "size": new java.awt.Dimension( 410, 785 )
- } )
- }
-}
diff --git a/src/main/java/edu/kit/kastel/extensions/guis/SettingsContent.java b/src/main/java/edu/kit/kastel/extensions/guis/SettingsContent.java
deleted file mode 100644
index 3225c20..0000000
--- a/src/main/java/edu/kit/kastel/extensions/guis/SettingsContent.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.extensions.guis;
-
-import java.awt.*;
-import java.util.*;
-
-import javax.swing.*;
-
-import com.intellij.ui.*;
-import com.intellij.ui.components.*;
-import net.miginfocom.swing.*;
-
-/**
- * @author clemens
- */
-public class SettingsContent extends JPanel {
- public SettingsContent() {
- initComponents();
- }
-
- public JLabel getLoggedInLabel() {
- return loggedInLabel;
- }
-
- public JTextField getArtemisUrlInput() {
- return artemisUrlInput;
- }
-
- public JTextField getInputUsername() {
- return InputUsername;
- }
-
- public JPasswordField getInputPwd() {
- return inputPwd;
- }
-
- public JButton getBtnLogin() {
- return btnLogin;
- }
-
- public JSpinner getNumColsSlider() {
- return numColsSlider;
- }
-
- public ColorPanel getAnnotationColorPicker() {
- return annotationColorPicker;
- }
-
- public JButton getBtnLogout() {
- return btnLogout;
- }
-
- private void initComponents() {
- // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off
- // Generated using JFormDesigner Evaluation license - Clemens D
- ResourceBundle bundle = ResourceBundle.getBundle("guiStrings");
- label3 = new JBLabel();
- loggedInLabel = new JLabel();
- separator1 = new JSeparator();
- label4 = new JBLabel();
- artemisUrlInput = new JTextField();
- label1 = new JBLabel();
- InputUsername = new JTextField();
- label2 = new JBLabel();
- inputPwd = new JPasswordField();
- btnLogin = new JButton();
- separator2 = new JSeparator();
- btnLogout = new JButton();
- label5 = new JBLabel();
- numColsSlider = new JSpinner();
- label6 = new JBLabel();
- annotationColorPicker = new ColorPanel();
-
- //======== this ========
- setBorder(new javax.swing.border.CompoundBorder(new javax.swing.border.TitledBorder(new javax.swing.border
- .EmptyBorder(0,0,0,0), "JF\u006frmD\u0065sig\u006eer \u0045val\u0075ati\u006fn",javax.swing.border.TitledBorder.CENTER,javax
- .swing.border.TitledBorder.BOTTOM,new java.awt.Font("Dia\u006cog",java.awt.Font.BOLD,
- 12),java.awt.Color.red), getBorder())); addPropertyChangeListener(new java.beans
- .PropertyChangeListener(){@Override public void propertyChange(java.beans.PropertyChangeEvent e){if("\u0062ord\u0065r".equals(e.
- getPropertyName()))throw new RuntimeException();}});
- setLayout(new MigLayout(
- "fillx,insets 0,hidemode 3,align left top,gap 0 0",
- // columns
- "[109,fill]" +
- "[grow,fill]",
- // rows
- "[]" +
- "[]" +
- "[]" +
- "[top]" +
- "[top]" +
- "[top]" +
- "[]" +
- "[top]" +
- "[]" +
- "[]"));
-
- //---- label3 ----
- label3.setText(bundle.getString("DebugMenuContent.label3.text"));
- add(label3, "cell 0 0");
-
- //---- loggedInLabel ----
- loggedInLabel.setText(bundle.getString("DebugMenuContent.loggedInLabel.text"));
- loggedInLabel.setForeground(Color.red);
- add(loggedInLabel, "cell 1 0");
- add(separator1, "cell 0 1 2 1");
-
- //---- label4 ----
- label4.setText(bundle.getString("DebugMenuContent.label4.text"));
- add(label4, "cell 0 2");
-
- //---- artemisUrlInput ----
- artemisUrlInput.setText(bundle.getString("DebugMenuContent.artemisUrlInput.text"));
- add(artemisUrlInput, "cell 1 2");
-
- //---- label1 ----
- label1.setText(bundle.getString("labelUnameField"));
- add(label1, "cell 0 3,alignx left,growx 0");
- add(InputUsername, "cell 1 3,growx");
-
- //---- label2 ----
- label2.setText(bundle.getString("LabelPwdField"));
- add(label2, "cell 0 4,alignx left,growx 0");
- add(inputPwd, "cell 1 4,growx");
-
- //---- btnLogin ----
- btnLogin.setText(bundle.getString("DebugMenuContent.btnLogin.text"));
- add(btnLogin, "cell 0 5 2 1");
- add(separator2, "cell 0 6 2 1");
-
- //---- btnLogout ----
- btnLogout.setText(bundle.getString("DebugMenuContent.btnLogout.text"));
- add(btnLogout, "cell 0 7 2 1");
-
- //---- label5 ----
- label5.setText(bundle.getString("DebugMenuContent.label5.text"));
- add(label5, "cell 0 8");
-
- //---- numColsSlider ----
- numColsSlider.setModel(new SpinnerNumberModel(2, 1, null, 1));
- add(numColsSlider, "cell 1 8");
-
- //---- label6 ----
- label6.setText(bundle.getString("AnnotationColor"));
- add(label6, "cell 0 9");
-
- //---- annotationColorPicker ----
- annotationColorPicker.setSelectedColor(new Color(0x9b3636));
- add(annotationColorPicker, "cell 1 9");
- // JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on
- }
-
- // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off
- // Generated using JFormDesigner Evaluation license - Clemens D
- private JBLabel label3;
- private JLabel loggedInLabel;
- private JSeparator separator1;
- private JBLabel label4;
- private JTextField artemisUrlInput;
- private JBLabel label1;
- private JTextField InputUsername;
- private JBLabel label2;
- private JPasswordField inputPwd;
- private JButton btnLogin;
- private JSeparator separator2;
- private JButton btnLogout;
- private JBLabel label5;
- private JSpinner numColsSlider;
- private JBLabel label6;
- private ColorPanel annotationColorPicker;
- // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on
-}
diff --git a/src/main/java/edu/kit/kastel/extensions/guis/SettingsContent.jfd b/src/main/java/edu/kit/kastel/extensions/guis/SettingsContent.jfd
deleted file mode 100644
index 8ecac6e..0000000
--- a/src/main/java/edu/kit/kastel/extensions/guis/SettingsContent.jfd
+++ /dev/null
@@ -1,140 +0,0 @@
-JFDML JFormDesigner: "8.2.3.0.386" Java: "17.0.8" encoding: "UTF-8"
-
-new FormModel {
- "i18n.bundleName": "guiStrings"
- "i18n.keyPrefix": "DebugMenuContent"
- "i18n.autoExternalize": true
- contentType: "form/swing"
- root: new FormRoot {
- add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
- "$layoutConstraints": "fillx,insets 0,hidemode 3,align left top,gap 0 0"
- "$columnConstraints": "[109,fill][grow,fill]"
- "$rowConstraints": "[][][][top][top][top][][top][][]"
- } ) {
- name: "this"
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "label3"
- "text": new FormMessage( null, "DebugMenuContent.label3.text" )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 0"
- } )
- add( new FormComponent( "javax.swing.JLabel" ) {
- name: "loggedInLabel"
- "text": new FormMessage( null, "DebugMenuContent.loggedInLabel.text" )
- "foreground": sfield java.awt.Color red
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 0"
- } )
- add( new FormComponent( "javax.swing.JSeparator" ) {
- name: "separator1"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 1 2 1"
- } )
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "label4"
- "text": new FormMessage( null, "DebugMenuContent.label4.text" )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 2"
- } )
- add( new FormComponent( "javax.swing.JTextField" ) {
- name: "artemisUrlInput"
- "text": new FormMessage( null, "DebugMenuContent.artemisUrlInput.text" )
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 2"
- } )
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "label1"
- "text": new FormMessage( null, "labelUnameField" )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 3,alignx left,growx 0"
- } )
- add( new FormComponent( "javax.swing.JTextField" ) {
- name: "InputUsername"
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 3,growx"
- } )
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "label2"
- "text": new FormMessage( null, "LabelPwdField" )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 4,alignx left,growx 0"
- } )
- add( new FormComponent( "javax.swing.JPasswordField" ) {
- name: "inputPwd"
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 4,growx"
- } )
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "btnLogin"
- "text": new FormMessage( null, "DebugMenuContent.btnLogin.text" )
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 5 2 1"
- } )
- add( new FormComponent( "javax.swing.JSeparator" ) {
- name: "separator2"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 6 2 1"
- } )
- add( new FormComponent( "javax.swing.JButton" ) {
- name: "btnLogout"
- "text": new FormMessage( null, "DebugMenuContent.btnLogout.text" )
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 7 2 1"
- } )
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "label5"
- "text": new FormMessage( null, "DebugMenuContent.label5.text" )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 8"
- } )
- add( new FormComponent( "javax.swing.JSpinner" ) {
- name: "numColsSlider"
- "model": new javax.swing.SpinnerNumberModel {
- minimum: 1
- value: 2
- }
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 8"
- } )
- add( new FormComponent( "com.intellij.ui.components.JBLabel" ) {
- name: "label6"
- "text": new FormMessage( null, "AnnotationColor" )
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 9"
- } )
- add( new FormComponent( "com.intellij.ui.ColorPanel" ) {
- name: "annotationColorPicker"
- "selectedColor": new java.awt.Color( 155, 54, 54, 255 )
- auxiliary() {
- "JavaCodeGenerator.variableGetter": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 9"
- } )
- }, new FormLayoutConstraints( null ) {
- "location": new java.awt.Point( 0, 0 )
- "size": new java.awt.Dimension( 465, 270 )
- } )
- }
-}
diff --git a/src/main/java/edu/kit/kastel/extensions/guis/StatisticsContainer.java b/src/main/java/edu/kit/kastel/extensions/guis/StatisticsContainer.java
deleted file mode 100644
index 6574f48..0000000
--- a/src/main/java/edu/kit/kastel/extensions/guis/StatisticsContainer.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.extensions.guis;
-
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.ui.components.JBLabel;
-import edu.kit.kastel.listeners.ExerciseSelectedListener;
-import edu.kit.kastel.sdq.artemis4j.api.ArtemisClientException;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.Exercise;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.ExerciseStats;
-import edu.kit.kastel.utils.ArtemisUtils;
-
-public class StatisticsContainer extends JBLabel {
-
- private static final String FETCH_STATS_FORMATTER = "Unable to fetch statistics for exercise %s";
-
- /**
- * Update the statistics label
- */
- public void triggerUpdate(Exercise selected) {
- try {
- ExerciseStats stats = ArtemisUtils.getArtemisClientInstance()
- .getAssessmentArtemisClient()
- .getStats(selected);
- this.setText(String.format(
- "Your submissions: %d | corrected: %d/%d | locked: %d",
- stats.submittedByTutor(), stats.totalAssessments(), stats.totalSubmissions(), stats.locked()));
- } catch (ArtemisClientException e) {
- ArtemisUtils.displayGenericErrorBalloon(String.format(FETCH_STATS_FORMATTER, selected.getShortName()));
- Logger.getInstance(ExerciseSelectedListener.class).error(e);
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/extensions/settings/ArtemisSettings.java b/src/main/java/edu/kit/kastel/extensions/settings/ArtemisSettings.java
deleted file mode 100644
index 5f55b48..0000000
--- a/src/main/java/edu/kit/kastel/extensions/settings/ArtemisSettings.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.extensions.settings;
-
-import java.awt.GridLayout;
-import java.awt.event.ActionEvent;
-
-import javax.swing.*;
-
-import com.intellij.openapi.options.Configurable;
-import com.intellij.openapi.options.ConfigurationException;
-import com.intellij.openapi.util.NlsContexts;
-import com.intellij.ui.ColorPanel;
-import com.intellij.ui.JBColor;
-import edu.kit.kastel.extensions.guis.SettingsContent;
-import edu.kit.kastel.login.CustomLoginManager;
-import edu.kit.kastel.sdq.artemis4j.api.ArtemisClientException;
-import edu.kit.kastel.sdq.artemis4j.client.RestClientManager;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * This class implements the settings Dialog for this PlugIn.
- * Everything directly related to the Setting UI should be in here.
- */
-public class ArtemisSettings implements Configurable {
-
- private static final String SETTINGS_DIALOG_NAME = "IntelliGrade Settings";
- private static final String LOGIN_ERROR_DIALOG_TITLE = "Error logging in!";
-
- // set up automated GUI and generate necessary bindings
- private final JPanel contentPanel = new JPanel();
- private final SettingsContent generatedMenu = new SettingsContent();
- private final JPasswordField pwdInput = generatedMenu.getInputPwd();
- private final JTextField usernameField = generatedMenu.getInputUsername();
- private final JLabel loggedInLabel = generatedMenu.getLoggedInLabel();
- private final JTextField artemisUrlField = generatedMenu.getArtemisUrlInput();
-
- private final JSpinner numColsSpinner = generatedMenu.getNumColsSlider();
-
- private final ColorPanel annotationColorSelector = generatedMenu.getAnnotationColorPicker();
-
- /**
- * Returns the visible name of the configurable component.
- * Note, that this method must return the display name
- * that is equal to the display name declared in XML
- * to avoid unexpected errors.
- *
- * @return the visible name of the configurable component
- */
- @Override
- public @NlsContexts.ConfigurableName String getDisplayName() {
- return SETTINGS_DIALOG_NAME;
- }
-
- /**
- * Creates a new Swing form that enables the user to configure the settings.
- * Usually this method is called on the EDT, so it should not take a long time.
- * Also, this place is designed to allocate resources (subscriptions/listeners etc.)
- *
- * @return new Swing form to show, or {@code null} if it cannot be created
- * @see #disposeUIResources
- */
- @Override
- public @Nullable JComponent createComponent() {
- contentPanel.setLayout(new GridLayout());
- contentPanel.add(generatedMenu);
- // add action listener to login Button
- generatedMenu.getBtnLogin().addActionListener(this::loginButtonListener);
- // add action listener to log out button
- generatedMenu.getBtnLogout().addActionListener(this::logOutButtonListener);
-
- return contentPanel;
- }
-
- /**
- * Listener Method that gets called when the login Button is pressed.
- * This method will Log in the User.
- *
- * @param actionEvent The Event passed by AWT is the Button is pressed
- */
- private void loginButtonListener(ActionEvent actionEvent) {
- // set label if login was successful
- setLabelOnLoginSuccess();
- }
-
- private void logOutButtonListener(ActionEvent actionEvent) {
- // reset username, password and token
- ArtemisSettingsState settings = ArtemisSettingsState.getInstance();
- settings.setUsername("");
- settings.setArtemisPassword("");
- settings.setArtemisAuthJWT("");
-
- // blank input fields
- this.pwdInput.setText("");
- this.usernameField.setText("");
- }
-
- /**
- * Indicates whether the Swing form was modified or not.
- * This method is called very often, so it should not take a long time.
- *
- * @return {@code true} if the settings were modified, {@code false} otherwise
- */
- @Override
- public boolean isModified() {
- ArtemisSettingsState settings = ArtemisSettingsState.getInstance();
- // check if all three parameters are equal
- String password = new String(pwdInput.getPassword());
- boolean modified = !password.equals(settings.getArtemisPassword());
- modified |= !usernameField.getText().equals(settings.getUsername());
- modified |= !artemisUrlField.getText().equals(settings.getArtemisInstanceUrl());
- modified |= !numColsSpinner.getValue().equals(settings.getColumnsPerRatingGroup());
- modified |= !annotationColorSelector.getSelectedColor().equals(settings.getAnnotationColor());
- return modified;
- }
-
- /**
- * Stores the settings from the Swing form to the configurable component.
- * This method is called on EDT upon user's request.
- *
- * @throws ConfigurationException if values cannot be applied
- */
- @Override
- public void apply() {
- // store all settings persistently
- ArtemisSettingsState settings = ArtemisSettingsState.getInstance();
- settings.setArtemisInstanceUrl(artemisUrlField.getText());
- settings.setUsername(usernameField.getText());
- settings.setArtemisPassword(new String(pwdInput.getPassword()));
- settings.setColumnsPerRatingGroup(
- Integer.parseInt(numColsSpinner.getValue().toString()));
- settings.setAnnotationColor(annotationColorSelector.getSelectedColor());
- }
-
- /**
- * Loads the settings from the configurable component to the Swing form.
- * This method is called on EDT immediately after the form creation or later upon user's request.
- */
- @Override
- public void reset() {
- ArtemisSettingsState settings = ArtemisSettingsState.getInstance();
- artemisUrlField.setText(settings.getArtemisInstanceUrl());
- usernameField.setText(settings.getUsername());
- pwdInput.setText(settings.getArtemisPassword());
- numColsSpinner.setValue(settings.getColumnsPerRatingGroup());
- annotationColorSelector.setSelectedColor(settings.getAnnotationColor());
-
- setLabelOnLoginSuccess();
- }
-
- /**
- * This method will create a new Connection to Artemis and try to log in.
- * If login was successful the label in the settings dialog will be set
- */
- private void setLabelOnLoginSuccess() {
-
- // create token based login manager
- var tokenLoginManager = new CustomLoginManager(
- artemisUrlField.getText(), usernameField.getText(), new String(pwdInput.getPassword()));
-
- // create new Artemis Instance
- var artemisInstance = new RestClientManager(artemisUrlField.getText(), tokenLoginManager);
-
- // try logging in and display error iff error occurred
- try {
- tokenLoginManager.login();
- } catch (ArtemisClientException e) {
- loggedInLabel.setText("false");
- loggedInLabel.setForeground(JBColor.RED);
- JOptionPane.showMessageDialog(
- contentPanel, e.getMessage(), LOGIN_ERROR_DIALOG_TITLE, JOptionPane.ERROR_MESSAGE);
- }
-
- if (artemisInstance.isReady()) {
- if (usernameField.getText().isBlank() || new String(pwdInput.getPassword()).isBlank()) {
- loggedInLabel.setText("true (logged in via Token)");
- } else {
- loggedInLabel.setText("true");
- }
- loggedInLabel.setForeground(JBColor.GREEN);
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/extensions/settings/ArtemisSettingsState.java b/src/main/java/edu/kit/kastel/extensions/settings/ArtemisSettingsState.java
deleted file mode 100644
index 4992969..0000000
--- a/src/main/java/edu/kit/kastel/extensions/settings/ArtemisSettingsState.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.extensions.settings;
-
-import java.awt.Color;
-import java.util.Date;
-
-import com.intellij.credentialStore.CredentialAttributes;
-import com.intellij.credentialStore.CredentialAttributesKt;
-import com.intellij.credentialStore.Credentials;
-import com.intellij.ide.passwordSafe.PasswordSafe;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.components.PersistentStateComponent;
-import com.intellij.openapi.components.State;
-import com.intellij.openapi.components.Storage;
-import com.intellij.ui.JBColor;
-import com.intellij.util.xmlb.XmlSerializer;
-import com.intellij.util.xmlb.XmlSerializerUtil;
-import org.jetbrains.annotations.Contract;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * This class persists all required data for the PlugIn.
- * Secrets (such as the Artemis password) are handled by the IntelliJ secrets provider
- */
-@State(name = "edu.kit.kastel.extensions.ArtemisSettingsState", storages = @Storage("IntelliGradeSettings.xml"))
-public class ArtemisSettingsState implements PersistentStateComponent {
-
- private static final String PASSWORD_STORE_KEY = "artemisPassword";
- private static final String CREDENTIALS_PATH = "edu.kit.kastel.intelligrade.artemisCredentials";
-
- private static final String JWT_STORE_KEY = "artemisAuthJWT";
-
- private String username = "";
- private String artemisInstanceUrl = "https://artemis.praktomat.cs.kit.edu";
- private @Nullable String selectedGradingConfigPath;
- private int columnsPerRatingGroup = 2;
-
- private Date jwtExpiry = new Date(Long.MAX_VALUE);
-
- private Color annotationColor = new JBColor(new Color(155, 54, 54), new Color(155, 54, 54));
-
- public static ArtemisSettingsState getInstance() {
- return ApplicationManager.getApplication().getService(ArtemisSettingsState.class);
- }
-
- /**
- * Gets the Settings state.
- *
- * @return a component state.
- * All properties, public and annotated fields are serialized.
- * Only values which differ from the default (i.e. the value of newly instantiated class)
- * are serialized. {@code null} value indicates that the returned state won't be stored,
- * as a result previously stored state will be used.
- * @see XmlSerializer
- */
- @Override
- public @Nullable ArtemisSettingsState getState() {
- return this;
- }
-
- /**
- * This method is called when a new component state is loaded.
- * The method can and will be called several times if config
- * files are externally changed while the IDE is running.
- * State object should be used directly, defensive copying is not required.
- *
- * @param state loaded component state
- * @see XmlSerializerUtil#copyBean(Object, Object)
- */
- @Override
- public void loadState(@NotNull ArtemisSettingsState state) {
- XmlSerializerUtil.copyBean(state, this);
- }
-
- @Contract("_ -> new")
- private @NotNull CredentialAttributes createCredentialAttributes(String key) {
- return new CredentialAttributes(CredentialAttributesKt.generateServiceName(CREDENTIALS_PATH, key));
- }
-
- public String getUsername() {
- return username;
- }
-
- public void setUsername(String username) {
- this.username = username;
- }
-
- /**
- * Get the password for the Artemis instance from the IDEs CredentialStore.
- *
- * @return the Password stored under the key {@value PASSWORD_STORE_KEY}
- */
- public String getArtemisPassword() {
- CredentialAttributes credentialAttributes = createCredentialAttributes(PASSWORD_STORE_KEY);
- return PasswordSafe.getInstance().getPassword(credentialAttributes);
- }
-
- /**
- * Store the provided Password securely into the IDEs
- * Credential Store under the key {@value PASSWORD_STORE_KEY}.
- *
- * @param artemisPassword the password to be stored
- */
- public void setArtemisPassword(String artemisPassword) {
- CredentialAttributes credentialAttributes = createCredentialAttributes(PASSWORD_STORE_KEY);
- Credentials credentials = new Credentials(username, artemisPassword);
- PasswordSafe.getInstance().set(credentialAttributes, credentials);
- }
-
- public synchronized void setArtemisAuthJWT(String jwt) {
- CredentialAttributes credentialAttributes = createCredentialAttributes(JWT_STORE_KEY);
- PasswordSafe.getInstance().setPassword(credentialAttributes, jwt);
- }
-
- public synchronized String getArtemisAuthJWT() {
- CredentialAttributes credentialAttributes = createCredentialAttributes(JWT_STORE_KEY);
- return PasswordSafe.getInstance().getPassword(credentialAttributes);
- }
-
- public String getArtemisInstanceUrl() {
- return artemisInstanceUrl;
- }
-
- public void setArtemisInstanceUrl(String artemisInstanceUrl) {
- // invalidate JWT if URL changed
- this.setArtemisAuthJWT("");
- this.artemisInstanceUrl = artemisInstanceUrl;
- }
-
- public @Nullable String getSelectedGradingConfigPath() {
- return selectedGradingConfigPath;
- }
-
- public int getColumnsPerRatingGroup() {
- return columnsPerRatingGroup;
- }
-
- public void setColumnsPerRatingGroup(int columnsPerRatingGroup) {
- this.columnsPerRatingGroup = columnsPerRatingGroup;
- }
-
- public void setSelectedGradingConfigPath(@Nullable String selectedGradingConfigPath) {
- this.selectedGradingConfigPath = selectedGradingConfigPath;
- }
-
- public Color getAnnotationColor() {
- return annotationColor;
- }
-
- public void setAnnotationColor(Color annotationColor) {
- this.annotationColor = annotationColor;
- }
-
- public Date getJwtExpiry() {
- return jwtExpiry;
- }
-
- public void setJwtExpiry(Date jwtExpiry) {
- this.jwtExpiry = jwtExpiry;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/extensions/tool_windows/AnnotationsToolWindowFactory.java b/src/main/java/edu/kit/kastel/extensions/tool_windows/AnnotationsToolWindowFactory.java
deleted file mode 100644
index 4f14b5d..0000000
--- a/src/main/java/edu/kit/kastel/extensions/tool_windows/AnnotationsToolWindowFactory.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.extensions.tool_windows;
-
-import java.awt.GridLayout;
-
-import javax.swing.JPanel;
-
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.wm.ToolWindow;
-import com.intellij.openapi.wm.ToolWindowFactory;
-import com.intellij.ui.content.Content;
-import com.intellij.ui.content.ContentFactory;
-import edu.kit.kastel.extensions.guis.AnnotationsViewContent;
-import edu.kit.kastel.utils.ArtemisUtils;
-import edu.kit.kastel.utils.AssessmentUtils;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * This class generates the tool Window for annotations in the bottom.
- */
-public class AnnotationsToolWindowFactory implements ToolWindowFactory {
-
- // set up automated GUI and generate necessary bindings
- private final JPanel contentPanel = new JPanel();
-
- private final AnnotationsViewContent generatedMenu = new AnnotationsViewContent();
-
- @Override
- public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
- contentPanel.setLayout(new GridLayout());
-
- // give up if logging in to Artemis failed
- if (!ArtemisUtils.getArtemisClientInstance().isReady()) {
- return;
- }
-
- // add content to menu panel
- contentPanel.add(generatedMenu);
- Content content = ContentFactory.getInstance().createContent(this.contentPanel, null, false);
-
- toolWindow.getContentManager().addContent(content);
-
- // register the GUI to be updated when an annotation is added
- AssessmentUtils.registerAssessmentListener(generatedMenu);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/extensions/tool_windows/MainToolWindowFactory.java b/src/main/java/edu/kit/kastel/extensions/tool_windows/MainToolWindowFactory.java
deleted file mode 100644
index 37ee78d..0000000
--- a/src/main/java/edu/kit/kastel/extensions/tool_windows/MainToolWindowFactory.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.extensions.tool_windows;
-
-import java.awt.GridLayout;
-import java.util.List;
-import java.util.Objects;
-
-import javax.swing.JButton;
-import javax.swing.JPanel;
-
-import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.ui.ComboBox;
-import com.intellij.openapi.ui.TextBrowseFolderListener;
-import com.intellij.openapi.ui.TextFieldWithBrowseButton;
-import com.intellij.openapi.wm.ToolWindow;
-import com.intellij.openapi.wm.ToolWindowFactory;
-import com.intellij.ui.content.Content;
-import com.intellij.ui.content.ContentFactory;
-import edu.kit.kastel.extensions.guis.AssessmentViewContent;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-import edu.kit.kastel.listeners.ExerciseSelectedListener;
-import edu.kit.kastel.listeners.GradingConfigSelectedListener;
-import edu.kit.kastel.listeners.OnSaveAssessmentBtnClick;
-import edu.kit.kastel.listeners.OnSubmitAssessmentBtnClick;
-import edu.kit.kastel.listeners.StartAssesment1Listener;
-import edu.kit.kastel.sdq.artemis4j.api.ArtemisClientException;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.Course;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.Exercise;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.exam.Exam;
-import edu.kit.kastel.state.AssessmentModeHandler;
-import edu.kit.kastel.utils.ArtemisUtils;
-import edu.kit.kastel.wrappers.Displayable;
-import edu.kit.kastel.wrappers.DisplayableCourse;
-import edu.kit.kastel.wrappers.DisplayableExam;
-import edu.kit.kastel.wrappers.DisplayableExercise;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * This class handles all logic for the main grading UI.
- * It does not handle any other logic, that should be factored out.
- */
-public class MainToolWindowFactory implements ToolWindowFactory {
-
- private static final String EXAMS_FETCH_ERROR_FORMAT = "Unable to fetch Exams for course %s.";
- private static final String EXERCISES_FETCH_ERROR_FORMAT = "Unable to fetch exercises for course %s.";
-
- // set up automated GUI and generate necessary bindings
- private final JPanel contentPanel = new JPanel();
- private final AssessmentViewContent generatedMenu = new AssessmentViewContent();
- private final TextFieldWithBrowseButton gradingConfigInput = generatedMenu.getGradingConfigPathInput();
- private final TextFieldWithBrowseButton autograderConfigInput = generatedMenu.getAutograderConfigPathInput();
- private final ComboBox> coursesComboBox = generatedMenu.getCoursesDropdown();
- private final ComboBox> examsComboBox = generatedMenu.getExamsDropdown();
- private final ComboBox> exerciseComboBox = generatedMenu.getExercisesDropdown();
-
- private final JButton startAssessment1Btn = generatedMenu.getBtnGradingRound1();
-
- private final JButton saveAssessmentBtn = generatedMenu.getBtnSaveAssessment();
-
- private final JButton submitAssessmentBtn = generatedMenu.getSubmitAssesmentBtn();
-
- @Override
- public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
- contentPanel.setLayout(new GridLayout());
-
- // give up if logging in to Artemis failed
- if (!ArtemisUtils.getArtemisClientInstance().isReady()) {
- return;
- }
-
- // add content to menu panel
- contentPanel.add(generatedMenu);
- Content content = ContentFactory.getInstance().createContent(this.contentPanel, null, false);
-
- addListeners();
- try {
- populateDropdowns();
- } catch (ArtemisClientException exc) {
- ArtemisUtils.displayLoginErrorBalloon("Error retrieving courses!", null);
- }
-
- // connect label to AssessmentModeHandler
- AssessmentModeHandler.getInstance().connectIndicatorLabel(generatedMenu.getAssessmentModeLabel());
-
- toolWindow.getContentManager().addContent(content);
- }
-
- private void addListeners() {
- gradingConfigInput.addBrowseFolderListener(
- new TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor("json")));
- autograderConfigInput.addBrowseFolderListener(
- new TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor("json")));
-
- // why the heck would you add a listener for text change like this????
- gradingConfigInput
- .getTextField()
- .getDocument()
- .addDocumentListener(new GradingConfigSelectedListener(gradingConfigInput));
-
- // set config path saved in settings
- ArtemisSettingsState settings = ArtemisSettingsState.getInstance();
- gradingConfigInput.setText(settings.getSelectedGradingConfigPath());
-
- // parse config on exercise select
- exerciseComboBox.addItemListener(new ExerciseSelectedListener(generatedMenu));
-
- // add listener for Button that starts first grading round
- startAssessment1Btn.addActionListener(new StartAssesment1Listener(generatedMenu));
-
- // button that saves assessment
- saveAssessmentBtn.addActionListener(new OnSaveAssessmentBtnClick());
-
- // button that submits assessment
- submitAssessmentBtn.addActionListener(new OnSubmitAssessmentBtnClick(generatedMenu.getStatisticsContainer()));
- }
-
- private void populateDropdowns() throws ArtemisClientException {
- coursesComboBox.removeAllItems();
- // add all courses to the courses dropdown
- List courses =
- ArtemisUtils.getArtemisClientInstance().getCourseArtemisClient().getCourses();
- courses.forEach(course -> coursesComboBox.addItem(new DisplayableCourse(course)));
-
- // populate exam and exercise dropdown if a course is selected
- coursesComboBox.addItemListener(itemEvent -> {
- Course selectedCourse = ((DisplayableCourse) itemEvent.getItem()).getWrappedValue();
- populateExamDropdown(selectedCourse);
- populateExercisesDropdown(selectedCourse);
- });
-
- // populate the dropdowns once because on load event listener is not triggered
- Course initial =
- ((DisplayableCourse) Objects.requireNonNull(coursesComboBox.getSelectedItem())).getWrappedValue();
- populateExamDropdown(initial);
- populateExercisesDropdown(initial);
- }
-
- private void populateExamDropdown(@NotNull Course course) {
- examsComboBox.removeAllItems();
- // we usually do not want to select the exam. Whe thus create a null Item
- examsComboBox.addItem(new DisplayableExam(null));
- try {
- // try to add all exams to the dropdown or fail
- course.getExams().forEach(exam -> examsComboBox.addItem(new DisplayableExam(exam)));
- } catch (ArtemisClientException e) {
- ArtemisUtils.displayGenericErrorBalloon(String.format(EXAMS_FETCH_ERROR_FORMAT, course));
- }
- }
-
- private void populateExercisesDropdown(@NotNull Course course) {
- exerciseComboBox.removeAllItems();
-
- try {
- course.getExercises().forEach(exercise -> exerciseComboBox.addItem(new DisplayableExercise(exercise)));
- } catch (ArtemisClientException e) {
- ArtemisUtils.displayGenericErrorBalloon(String.format(EXERCISES_FETCH_ERROR_FORMAT, course));
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/listeners/ExerciseSelectedListener.java b/src/main/java/edu/kit/kastel/listeners/ExerciseSelectedListener.java
deleted file mode 100644
index 4fb97b8..0000000
--- a/src/main/java/edu/kit/kastel/listeners/ExerciseSelectedListener.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.listeners;
-
-import java.awt.GridLayout;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.io.IOException;
-import java.util.Comparator;
-import java.util.Objects;
-
-import javax.swing.BorderFactory;
-import javax.swing.JButton;
-import javax.swing.JPanel;
-
-import com.intellij.DynamicBundle;
-import com.intellij.ui.JBColor;
-import edu.kit.kastel.extensions.guis.AssessmentViewContent;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.Exercise;
-import edu.kit.kastel.sdq.artemis4j.grading.config.ExerciseConfig;
-import edu.kit.kastel.sdq.artemis4j.grading.config.JsonFileConfig;
-import edu.kit.kastel.sdq.artemis4j.grading.model.MistakeType;
-import edu.kit.kastel.state.AssessmentModeHandler;
-import edu.kit.kastel.utils.ArtemisUtils;
-import edu.kit.kastel.wrappers.DisplayableExercise;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * This listener handles selection of a new exercise.
- */
-public class ExerciseSelectedListener implements ItemListener {
- private static @Nullable JsonFileConfig fileConfig;
-
- private static final String CONFIG_FILE_LOAD_ERROR_FORMATTER = "Error loading config at %s!";
- private static final String NO_EXERCISE_SELECTED_ERROR = "Please select an exercise to begin grading.";
-
- private static final String EXERCISE_INVALID_FORMATTER = "You are trying to grade \"%s\" with a config for \"%s\"!";
-
- private static final String LOCALE = DynamicBundle.getLocale().getLanguage();
-
- /**
- * Hold a reference to the UI, so we can dynamically modify components.
- */
- private final AssessmentViewContent gui;
-
- public ExerciseSelectedListener(AssessmentViewContent gui) {
- this.gui = gui;
- }
-
- @Override
- public void itemStateChanged(@NotNull ItemEvent itemEvent) {
- // to avoid errors when Item is only deselected
- if (itemEvent.getStateChange() == ItemEvent.DESELECTED) {
- return;
- }
-
- ExerciseConfig configForExercise;
- Exercise selected = ((DisplayableExercise) Objects.requireNonNull(itemEvent.getItem())).getWrappedValue();
-
- // we cannot load a config if no exercise is selected
- if (ExerciseSelectedListener.fileConfig == null) {
- ArtemisUtils.displayGenericErrorBalloon(NO_EXERCISE_SELECTED_ERROR);
- return;
- }
-
- // try loading the config for an exercise
- try {
- configForExercise = ExerciseSelectedListener.fileConfig.getExerciseConfig(selected);
- } catch (IOException e) {
- ArtemisUtils.displayGenericErrorBalloon(String.format(
- ExerciseSelectedListener.CONFIG_FILE_LOAD_ERROR_FORMATTER,
- ArtemisSettingsState.getInstance().getSelectedGradingConfigPath()));
- // stop any further parse attempts if loading file failed
- return;
- }
-
- // if the exercise that is to be graded is invalid for this Config
- if (!configForExercise.getAllowedExercises().contains(selected.getExerciseId())) {
- // display error message
- ArtemisUtils.displayGenericErrorBalloon(String.format(
- EXERCISE_INVALID_FORMATTER, selected.getShortName(), configForExercise.getShortName()));
- // grey out assessment buttons
- gui.getBtnGradingRound1().setEnabled(false);
- return;
- }
- // enable assessment button because it may still be greyed out
- gui.getBtnGradingRound1().setEnabled(true);
- addRatingGroups(configForExercise);
- AssessmentModeHandler.getInstance().setCurrentExerciseConfig(configForExercise);
-
- // update statistics
- gui.getStatisticsContainer().triggerUpdate(selected);
- }
-
- private void addRatingGroups(@NotNull ExerciseConfig configForExercise) {
-
- // clear content before adding new
- gui.getRatingGroupContainer().removeAll();
-
- // add all rating groups
- configForExercise.getRatingGroups().stream()
- // only add assessment group if it is non-empty
- .filter(ratingGroup -> !ratingGroup.getMistakeTypes().isEmpty())
- .forEach(ratingGroup -> {
-
- // calculate grid size
- int colsPerRatingGroup = ArtemisSettingsState.getInstance().getColumnsPerRatingGroup();
- int numRows = ratingGroup.getMistakeTypes().size() / colsPerRatingGroup;
-
- // create a panel of appropriate size for each rating group
- JPanel ratingCroupContainer = new JPanel(new GridLayout(numRows + 1, colsPerRatingGroup));
-
- ratingCroupContainer.setBorder(BorderFactory.createTitledBorder(
- BorderFactory.createLineBorder(JBColor.LIGHT_GRAY),
- String.format(
- "%s [%.2f of %.2f]",
- ratingGroup.getDisplayName(LOCALE),
- ratingGroup.getRange().second(),
- ratingGroup.getRange().first())));
-
- // add buttons to rating group
- ratingGroup.getMistakeTypes().stream()
- // sort buttons alphabetically
- // TODO: for some reason this is broken
- .sorted(Comparator.comparing(mistake -> mistake.getButtonText(LOCALE)))
- .forEach(mistakeType -> {
- // create button, add listener and add it to the container
- JButton assessmentButton = new JButton(mistakeType.getButtonText(LOCALE));
- assessmentButton.addActionListener(
- new OnAssesmentButtonClickListener((MistakeType) mistakeType));
- ratingCroupContainer.add(assessmentButton);
- });
-
- gui.getRatingGroupContainer().add(ratingCroupContainer);
- });
- }
-
- public static synchronized void updateJsonConfig(JsonFileConfig jsonFileConfig) {
- ExerciseSelectedListener.fileConfig = jsonFileConfig;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/listeners/OnAssesmentButtonClickListener.java b/src/main/java/edu/kit/kastel/listeners/OnAssesmentButtonClickListener.java
deleted file mode 100644
index 8389c5c..0000000
--- a/src/main/java/edu/kit/kastel/listeners/OnAssesmentButtonClickListener.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.listeners;
-
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-
-import edu.kit.kastel.sdq.artemis4j.grading.model.MistakeType;
-import edu.kit.kastel.utils.AnnotationUtils;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * This class represents a generic listener that is called if an assessment button
- * is clicked. It should create and save the new annotation.
- */
-public class OnAssesmentButtonClickListener implements ActionListener {
-
- private final MistakeType mistakeType;
-
- public OnAssesmentButtonClickListener(MistakeType mistakeType) {
- this.mistakeType = mistakeType;
- }
-
- @Override
- public void actionPerformed(@NotNull ActionEvent actionEvent) {
- AnnotationUtils.addAnnotationByMistakeType(this.mistakeType);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/listeners/OnSaveAssessmentBtnClick.java b/src/main/java/edu/kit/kastel/listeners/OnSaveAssessmentBtnClick.java
deleted file mode 100644
index a311f6a..0000000
--- a/src/main/java/edu/kit/kastel/listeners/OnSaveAssessmentBtnClick.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.listeners;
-
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-
-import edu.kit.kastel.state.AssessmentModeHandler;
-import edu.kit.kastel.utils.ArtemisUtils;
-
-/**
- * Listener to be called if the Assessment is to be saved.
- */
-public class OnSaveAssessmentBtnClick implements ActionListener {
-
- private static final String NO_ASSESSMENT_OPEN_ERR = "You are currently not grading an assessment.";
-
- @Override
- public void actionPerformed(ActionEvent actionEvent) {
- if (!AssessmentModeHandler.getInstance().isInAssesmentMode()) {
- ArtemisUtils.displayGenericErrorBalloon(NO_ASSESSMENT_OPEN_ERR);
- }
- // TODO: implement
- }
-}
diff --git a/src/main/java/edu/kit/kastel/listeners/OnSubmitAssessmentBtnClick.java b/src/main/java/edu/kit/kastel/listeners/OnSubmitAssessmentBtnClick.java
deleted file mode 100644
index ff1994f..0000000
--- a/src/main/java/edu/kit/kastel/listeners/OnSubmitAssessmentBtnClick.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.listeners;
-
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.io.IOException;
-import java.util.Optional;
-
-import com.intellij.openapi.diagnostic.Logger;
-import edu.kit.kastel.extensions.guis.StatisticsContainer;
-import edu.kit.kastel.sdq.artemis4j.api.ArtemisClientException;
-import edu.kit.kastel.sdq.artemis4j.grading.artemis.AnnotationMapper;
-import edu.kit.kastel.sdq.artemis4j.grading.config.ExerciseConfig;
-import edu.kit.kastel.state.AssessmentModeHandler;
-import edu.kit.kastel.utils.ArtemisUtils;
-import edu.kit.kastel.utils.AssessmentUtils;
-
-public class OnSubmitAssessmentBtnClick implements ActionListener {
-
- private static final String ARTEMIS_ERROR_STRING = "An error occurred submitting the assessment to Artemis.";
-
- private static final String IO_ERROR_STRING = "Error creating assessment result";
-
- private static final String ERROR_NOT_ASSESSING =
- "Error obtaining exercise config. Are you currently assessing a submission?";
-
- private final StatisticsContainer statisticsContainer;
-
- public OnSubmitAssessmentBtnClick(StatisticsContainer statisticsContainer) {
- this.statisticsContainer = statisticsContainer;
- }
-
- @Override
- public void actionPerformed(ActionEvent actionEvent) {
- // submit iff a lock is present
- AssessmentModeHandler.getInstance().getAssessmentLock().ifPresent(lockResult -> {
-
- // trigger a statistics update
- this.statisticsContainer.triggerUpdate(lockResult.getExercise());
-
- Optional config = AssessmentUtils.getConfigAsExerciseCfg();
-
- // only assess if the exercise config can be obtained
- config.ifPresentOrElse(
- exerciseConfig -> {
- try {
- // create assessment results
- AnnotationMapper annotationMapper = new AnnotationMapper(
- lockResult.getExercise(),
- lockResult.getSubmission(),
- AssessmentUtils.getAllAnnotations(),
- exerciseConfig.getIRatingGroups(),
- ArtemisUtils.getArtemisClientInstance()
- .getAuthenticationClient()
- .getUser(),
- lockResult.getSubmissionLock());
- // save the assessment
- ArtemisUtils.getArtemisClientInstance()
- .getAssessmentArtemisClient()
- .saveAssessment(
- lockResult.getLockedSubmissionId(),
- true,
- annotationMapper.createAssessmentResult());
- } catch (ArtemisClientException ace) {
- Logger.getInstance(OnSubmitAssessmentBtnClick.class).error(ace);
- ArtemisUtils.displayGenericErrorBalloon(ARTEMIS_ERROR_STRING);
- } catch (IOException ioe) {
- Logger.getInstance(OnSubmitAssessmentBtnClick.class).error(ioe);
- ArtemisUtils.displayGenericErrorBalloon(IO_ERROR_STRING);
- }
-
- // disable the assessment mode
- AssessmentModeHandler.getInstance().disableAssessmentMode();
- },
- () -> ArtemisUtils.displayGenericErrorBalloon(ERROR_NOT_ASSESSING));
- });
- }
-}
diff --git a/src/main/java/edu/kit/kastel/listeners/StartAssesment1Listener.java b/src/main/java/edu/kit/kastel/listeners/StartAssesment1Listener.java
deleted file mode 100644
index a0332e4..0000000
--- a/src/main/java/edu/kit/kastel/listeners/StartAssesment1Listener.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.listeners;
-
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Comparator;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-import com.intellij.ide.impl.ProjectUtil;
-import com.intellij.notification.NotificationGroupManager;
-import com.intellij.notification.NotificationType;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.ui.MessageDialogBuilder;
-import edu.kit.kastel.extensions.guis.AssessmentViewContent;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-import edu.kit.kastel.sdq.artemis4j.api.ArtemisClientException;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.Exercise;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.assessment.LockResult;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.assessment.Submission;
-import edu.kit.kastel.sdq.artemis4j.api.client.IAssessmentArtemisClient;
-import edu.kit.kastel.state.AssessmentModeHandler;
-import edu.kit.kastel.utils.ArtemisUtils;
-import edu.kit.kastel.utils.AssessmentUtils;
-import edu.kit.kastel.wrappers.Displayable;
-import edu.kit.kastel.wrappers.ExtendedLockResult;
-import org.eclipse.jgit.api.CloneCommand;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.api.errors.InvalidRemoteException;
-import org.eclipse.jgit.api.errors.TransportException;
-import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
-import org.jetbrains.annotations.NotNull;
-import org.slf4j.LoggerFactory;
-
-/**
- * Listener that gets called when the first grading round is started.
- */
-public class StartAssesment1Listener implements ActionListener {
-
- private static final String NO_CONFIG_SELECTED_MSG = "Please select the appropriate grading config";
-
- private static final String SELECT_EXERCISE_MSG = "Please select an exercise to start grading";
-
- private static final String ERROR_NEXT_ASSESSMENT_FORMATTER = "Error requestung a new submission lock: %s "
- + "(this most likely means there are no more submissions to be graded)";
-
- private static final String GIT_ERROR_FORMATTER = "Error cloning submission \"%s\". Are you authenticated?";
-
- private static final String ERROR_DELETE_REPO_DIR = "Error deleting existing submission directory.";
-
- private static final String LOOSE_ASSESSMENT_MSG = "You already have an assessment loaded. Loading a new assessment"
- + " will cause you to loose all unsaved gradings! Load new assessment anyway?";
-
- private static final String LOCKED_BUT_NO_LOCK_RESULT_ERROR =
- "The submission has been locked but a LockResult could not be obtained. Please submit a GitHub Issue";
- private final AssessmentViewContent gui;
-
- private final IAssessmentArtemisClient assessmentClient =
- ArtemisUtils.getArtemisClientInstance().getAssessmentArtemisClient();
-
- public StartAssesment1Listener(AssessmentViewContent gui) {
- this.gui = gui;
- }
-
- @Override
- public void actionPerformed(ActionEvent actionEvent) {
-
- // check if any config is selected. If wrong config is selected Button will be unclickable
- if (gui.getGradingConfigPathInput().getText().isBlank()) {
- ArtemisUtils.displayGenericErrorBalloon(NO_CONFIG_SELECTED_MSG);
- return;
- }
-
- if (gui.getExercisesDropdown().getSelectedItem() == null) {
- ArtemisUtils.displayGenericErrorBalloon(SELECT_EXERCISE_MSG);
- return;
- }
-
- // check if an assessment is already loaded
- if (AssessmentModeHandler.getInstance().isInAssesmentMode()
- && !MessageDialogBuilder.yesNo("Unsaved assessment", LOOSE_ASSESSMENT_MSG)
- .guessWindowAndAsk()) {
- return;
- }
-
- // get the assessment and try to obtain a lock
- Exercise selectedExercise =
- ((Displayable) gui.getExercisesDropdown().getSelectedItem()).getWrappedValue();
-
- Optional lockedSubmissionIdWrapper;
- try {
-
- // TODO: Calculate if assessments are still to be graded or not
-
- lockedSubmissionIdWrapper = assessmentClient.startNextAssessment(selectedExercise, 0);
-
- } catch (ArtemisClientException e) {
- ArtemisUtils.displayGenericErrorBalloon(String.format(ERROR_NEXT_ASSESSMENT_FORMATTER, e.getMessage()));
- return;
- }
-
- if (lockedSubmissionIdWrapper.isEmpty()) {
- ArtemisUtils.displayGenericErrorBalloon(
- // TODO: correct error message
- String.format(ERROR_NEXT_ASSESSMENT_FORMATTER, ""));
- return;
- }
-
- // process the submission iff a lock was obtained
- Integer lockedAssessmentId = lockedSubmissionIdWrapper.get();
-
- // attempt to truly obtain the lock for the submission
- Optional lockWrapper = Optional.empty();
- try {
- lockWrapper = Optional.ofNullable(assessmentClient.startAssessment(ArtemisUtils.getArtemisClientInstance()
- .getSubmissionArtemisClient()
- .getSubmissionById(selectedExercise, lockedAssessmentId)));
- } catch (ArtemisClientException e) {
- ArtemisUtils.displayGenericErrorBalloon(LOCKED_BUT_NO_LOCK_RESULT_ERROR);
- Logger.getInstance(StartAssesment1Listener.class).error(e);
- }
-
- Optional repoUrlWrapper = getRepoUrlFromAssessmentLock(lockedAssessmentId, selectedExercise);
-
- // obtain student name or use submission number
- String repoIdentifier;
- try {
- repoIdentifier = ArtemisUtils.getArtemisClientInstance()
- .getSubmissionArtemisClient()
- .getSubmissionById(selectedExercise, lockedAssessmentId)
- .getParticipantIdentifier();
- } catch (ArtemisClientException e) {
- repoIdentifier = Integer.toString(lockedAssessmentId);
- }
-
- String repositoryName = String.format("%s_%s", selectedExercise.getShortName(), repoIdentifier);
-
- repoUrlWrapper.ifPresent(repoUrl -> this.cloneSubmissionToTempdir(repoUrl, repositoryName));
-
- // set assessment mode
- lockWrapper.ifPresent(submissionLock -> AssessmentModeHandler.getInstance()
- .enableAssessmentMode(new ExtendedLockResult(lockedAssessmentId, selectedExercise, submissionLock)));
-
- // run Autograder
- runAutograder();
-
- // update statistics
- gui.getStatisticsContainer().triggerUpdate(selectedExercise);
- }
-
- /**
- * Use an assessmentLock to get the repository URL of a submission.
- *
- * @param lockedSubmissionId The id of the submission that has been locked
- * @param selectedExercise The exercise this submission belongs to
- * @return Optional#empty if an error occurred, The wrapped string otherwise
- */
- private Optional getRepoUrlFromAssessmentLock(
- @NotNull Integer lockedSubmissionId, @NotNull Exercise selectedExercise) {
-
- Optional repoUrl = Optional.empty();
- try {
- // try to get the repository URL of the submission
- Submission toBeGraded = ArtemisUtils.getArtemisClientInstance()
- .getSubmissionArtemisClient()
- .getSubmissionById(selectedExercise, lockedSubmissionId);
- repoUrl = Optional.of(toBeGraded.getRepositoryUrl());
- } catch (ArtemisClientException e) {
- // display Error Balloon on failure
- ArtemisUtils.displayGenericErrorBalloon(String.format(ERROR_NEXT_ASSESSMENT_FORMATTER, e.getMessage()));
- }
-
- return repoUrl;
- }
-
- private void cloneSubmissionToTempdir(@NotNull String repoUrl, @NotNull String repoName) {
- // construct repo path
- File submissionRepoDir = new File(String.valueOf(Path.of(System.getProperty("java.io.tmpdir"), repoName)));
-
- // delete repository folder if it exists
- if (submissionRepoDir.exists()) {
- try (Stream files = Files.walk(submissionRepoDir.toPath())) {
- files.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
- } catch (IOException e) {
- ArtemisUtils.displayGenericErrorBalloon(ERROR_DELETE_REPO_DIR);
- }
- }
-
- // try to clone the repository
- CloneCommand cloner = Git.cloneRepository()
- .setURI(repoUrl)
- .setDirectory(submissionRepoDir)
- .setCredentialsProvider(new UsernamePasswordCredentialsProvider(
- ArtemisSettingsState.getInstance().getUsername(),
- ArtemisSettingsState.getInstance().getArtemisPassword()));
- // generate notification because cloning is slow
- NotificationGroupManager.getInstance()
- .getNotificationGroup("IntelliGrade Notifications")
- .createNotification("Cloning repository...\n This might take a while.", NotificationType.INFORMATION)
- .setTitle("Please wait")
- .notify(ProjectUtil.getActiveProject());
-
- try (Git ignored = cloner.call()) {
- // empty because of try with resource
- } catch (InvalidRemoteException | TransportException e) {
- ArtemisUtils.displayGenericErrorBalloon(String.format(GIT_ERROR_FORMATTER, e.getMessage()));
- return;
- } catch (GitAPIException e) {
- LoggerFactory.getLogger(StartAssesment1Listener.class).error(e.toString());
- return;
- }
-
- // document selected exercise
- int selectedIdx = this.gui.getExercisesDropdown().getSelectedIndex();
-
- // open project
- ProjectUtil.openOrImport(submissionRepoDir.toPath(), ProjectUtil.getActiveProject(), false);
-
- // reset listeners because they get registered when a new project is opened
- AssessmentUtils.resetAssessmentListeners();
-
- // generate notification because cloning is slow
- NotificationGroupManager.getInstance()
- .getNotificationGroup("IntelliGrade Notifications")
- .createNotification(
- String.format("Submission %s cloned successfully.", repoName), NotificationType.INFORMATION)
- .setTitle("Finished")
- .notify(ProjectUtil.getActiveProject());
-
- // set correct exercise in panel (new IDE instance resets this)
- this.gui.getExercisesDropdown().setSelectedIndex(selectedIdx);
- }
-
- private void runAutograder() {
- /*
- //TODO: boilerplate implementation
- try (TempLocation tempProjectLocation = TempLocation.random()) {
- Application autograderInstance = new Application(tempProjectLocation);
- Future resultCode = Executors.newSingleThreadExecutor().submit(autograderInstance);
- resultCode.get();
- } catch (IOException e) {
- //If creation of temp dir fails
- } catch (ExecutionException e) {
- //if autograder Throws Exception
- } catch (InterruptedException e) {
- //If autograder is interrupted
- }
- */
- }
-}
diff --git a/src/main/java/edu/kit/kastel/login/CefUtils.java b/src/main/java/edu/kit/kastel/login/CefUtils.java
deleted file mode 100644
index c108592..0000000
--- a/src/main/java/edu/kit/kastel/login/CefUtils.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.login;
-
-import java.awt.GridLayout;
-import java.awt.Toolkit;
-import java.util.concurrent.Future;
-
-import javax.swing.JFrame;
-import javax.swing.JPanel;
-import javax.swing.WindowConstants;
-
-import com.intellij.ui.jcef.JBCefApp;
-import com.intellij.ui.jcef.JBCefBrowser;
-import com.intellij.ui.jcef.JBCefClient;
-import com.intellij.ui.jcef.JBCefCookie;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-import org.cef.handler.CefFocusHandler;
-import org.jetbrains.annotations.NotNull;
-
-public final class CefUtils {
-
- private static final double BROWSER_WINDOW_SCALE_FACTOR = 0.75f;
- private static final String BROWSER_LOGIN_TITLE = "Artemis log in";
-
- private CefUtils() {
- throw new IllegalAccessError("Utility Class");
- }
-
- /**
- * Create and display a Window containing a JBCef Window to request login.
- *
- * @return A future on the JWT Cookie to log in
- */
- public static @NotNull Future jcefBrowserLogin() throws InterruptedException {
-
- // create browser and browser Client
- JBCefClient browserClient = JBCefApp.getInstance().createClient();
- JBCefBrowser browser = JBCefBrowser.createBuilder()
- .setClient(browserClient)
- .setUrl(ArtemisSettingsState.getInstance().getArtemisInstanceUrl())
- .build();
-
- // add a handler to the Browser to be run if a page is loaded
- CefWindowLoadHandler loadHandler = new CefWindowLoadHandler(browser);
- browserClient.addLoadHandler(loadHandler, browser.getCefBrowser());
-
- // set focus handler because it gets invoked sometimes and causes NullPE otherwise
- CefFocusHandler focusHandler = new CefWindowFocusHandler();
- browserClient.addFocusHandler(focusHandler, browser.getCefBrowser());
-
- // create window, display it and navigate to log in URL
- createWindow(browser);
- return loadHandler.getCookieFuture();
- }
-
- private static void createWindow(@NotNull JBCefBrowser browserToAdd) {
- // build and display browser window
- JFrame browserContainerWindow = new JFrame(BROWSER_LOGIN_TITLE);
- browserContainerWindow.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
- browserContainerWindow.setSize(
- (int) Math.ceil(Toolkit.getDefaultToolkit().getScreenSize().getWidth() * BROWSER_WINDOW_SCALE_FACTOR),
- (int) Math.ceil(Toolkit.getDefaultToolkit().getScreenSize().getHeight() * BROWSER_WINDOW_SCALE_FACTOR));
- JPanel browserContainer = new JPanel(new GridLayout(1, 1));
- browserContainerWindow.add(browserContainer);
- browserContainer.add(browserToAdd.getComponent());
- browserContainerWindow.setVisible(true);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/login/CefWindowLoadHandler.java b/src/main/java/edu/kit/kastel/login/CefWindowLoadHandler.java
deleted file mode 100644
index e634a5b..0000000
--- a/src/main/java/edu/kit/kastel/login/CefWindowLoadHandler.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.login;
-
-import java.util.Optional;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-
-import com.intellij.ui.jcef.JBCefBrowser;
-import com.intellij.ui.jcef.JBCefCookie;
-import org.cef.browser.CefBrowser;
-import org.cef.browser.CefFrame;
-import org.cef.handler.CefLoadHandlerAdapter;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Class to handle callbacks when a site in the JBCef Browser is loaded. Use to start retrieving cookies.
- */
-public class CefWindowLoadHandler extends CefLoadHandlerAdapter {
-
- private final JBCefBrowser containingBrowser;
-
- private Optional> cookieFuture;
-
- CefWindowLoadHandler(JBCefBrowser browser) {
- this.containingBrowser = browser;
- this.cookieFuture = Optional.empty();
- }
-
- @Override
- public void onLoadEnd(@NotNull CefBrowser browser, CefFrame frame, int httpStatusCode) {
-
- // build and start a new Thread to retrieve the Cookies
- JwtRetriever cookieRetriever = new JwtRetriever(containingBrowser);
- // lock the Monitor
- synchronized (this) {
- // create future to retrieve Cookie
- // NOTE: using Callable instead of Runnable
- this.cookieFuture = Optional.of(Executors.newSingleThreadExecutor().submit(cookieRetriever));
- // wake up potentially waiting Threads that want to get the Cookie
- notifyAll();
- }
- }
-
- /**
- * Get the Future on the Cookie. Wait for it to become available if necessary.
- *
- * @return a Future on the JWT Cookie to be retrieved later
- * @throws InterruptedException if the call is interrupted while attempting to obtain the Cookie
- */
- public Future getCookieFuture() throws InterruptedException {
- // lock monitor
- synchronized (this) {
- // wait for the cookie to become available.
- // necessary because this method might be called before onLoadEnd
- while (this.cookieFuture.isEmpty()) {
- wait();
- }
- // return content of Optional
- return cookieFuture.get();
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/login/CustomLoginManager.java b/src/main/java/edu/kit/kastel/login/CustomLoginManager.java
deleted file mode 100644
index 7b78c36..0000000
--- a/src/main/java/edu/kit/kastel/login/CustomLoginManager.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.login;
-
-import java.time.Instant;
-import java.util.Date;
-import java.util.concurrent.ExecutionException;
-
-import com.intellij.notification.Notification;
-import com.intellij.notification.NotificationAction;
-import com.intellij.openapi.actionSystem.AnActionEvent;
-import com.intellij.openapi.options.ShowSettingsUtil;
-import com.intellij.ui.jcef.JBCefApp;
-import com.intellij.ui.jcef.JBCefCookie;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-import edu.kit.kastel.sdq.artemis4j.api.ArtemisClientException;
-import edu.kit.kastel.utils.ArtemisUtils;
-import org.jetbrains.annotations.NotNull;
-
-public class CustomLoginManager extends edu.kit.kastel.sdq.artemis4j.client.LoginManager {
-
- private static final String JFCE_UNAVAILABLE_ERRORMESSAGE =
- "Embedded browser unavailable. Please log in using username and password.";
-
- private static final ArtemisSettingsState settingsStore = ArtemisSettingsState.getInstance();
-
- public CustomLoginManager(String hostname, String username, String password) {
- super(hostname, username, password);
- }
-
- @Override
- public void login() throws ArtemisClientException {
-
- // before we log in we check if the jwt needs to be invalidated
- if (Date.from(Instant.now()).after(settingsStore.getJwtExpiry())) {
- settingsStore.setArtemisAuthJWT("");
- settingsStore.setJwtExpiry(new Date(Long.MAX_VALUE));
- }
-
- if (this.hostname.isBlank()) {
- throw new ArtemisClientException("Login without hostname is impossible");
- } else if (this.username == null
- || this.password == null
- || this.username.isBlank()
- || this.password.isBlank()) {
- // check log in data and open login Browser iff needed
- // only do browser login if no token is set and either username or password is not set as well
- if (settingsStore.getArtemisAuthJWT() == null
- || settingsStore.getArtemisAuthJWT().isBlank()) {
- if (JBCefApp.isSupported()) {
- ifCefAvailable();
- } else {
- // if JCEF is unavailable suggest usage of conventional login
- displayJCEFUnavailableError();
- }
- }
- // get Token from Settings, which should be set now
- this.token = settingsStore.getArtemisAuthJWT();
- } else {
- this.token = this.loginViaUsernameAndPassword();
- }
- this.assessor = this.fetchAssessor();
- }
-
- private void displayJCEFUnavailableError() {
- ArtemisUtils.displayLoginErrorBalloon(JFCE_UNAVAILABLE_ERRORMESSAGE, new NotificationAction("Log in...") {
- @Override
- public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) {
- ShowSettingsUtil.getInstance().showSettingsDialog(null, "IntelliGrade Settings");
- }
- });
- }
-
- private void ifCefAvailable() throws ArtemisClientException {
- try {
- // open the JCEF Browser in a new Panel to allow user to log in
- // wait for operation to complete
- JBCefCookie jwtCookie = CefUtils.jcefBrowserLogin().get();
-
- // set relevant information
- settingsStore.setArtemisAuthJWT(jwtCookie.getValue());
- settingsStore.setJwtExpiry(jwtCookie.getExpires());
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new ArtemisClientException("Interrupted while attempting to get login token");
- } catch (ExecutionException e) {
- throw new ArtemisClientException(String.format("Error retrieving token! %n%s", e.getMessage()));
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/login/JwtRetriever.java b/src/main/java/edu/kit/kastel/login/JwtRetriever.java
deleted file mode 100644
index d28b802..0000000
--- a/src/main/java/edu/kit/kastel/login/JwtRetriever.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.login;
-
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-
-import com.intellij.ui.jcef.JBCefBrowser;
-import com.intellij.ui.jcef.JBCefCookie;
-import com.intellij.ui.jcef.JBCefCookieManager;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-
-public class JwtRetriever implements Callable {
-
- private static final String JWT_COOKIE_KEY = "jwt";
-
- private final JBCefBrowser browser;
-
- private JBCefCookie cookieRetrieved;
-
- public JwtRetriever(JBCefBrowser browser) {
- this.browser = browser;
- }
-
- @Override
- public JBCefCookie call() throws Exception {
- ArtemisSettingsState settingsStore = ArtemisSettingsState.getInstance();
-
- JBCefCookieManager cookieManager = browser.getJBCefCookieManager();
- Future> cookies = cookieManager.getCookies(settingsStore.getArtemisInstanceUrl(), true);
- // get all cookies, search for the jwt and update it in the settings if necessary
- cookies.get().stream()
- .filter(cookie -> cookie.getName().equals(JWT_COOKIE_KEY))
- .forEach(cookie -> {
- String jwt = cookie.getValue();
- this.cookieRetrieved = cookie;
- if (!jwt.equals(settingsStore.getArtemisAuthJWT())) {
- this.browser.getJBCefClient().dispose();
- this.browser.dispose();
- }
- });
- return cookieRetrieved;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/actions/AddAnnotationPopupAction.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/actions/AddAnnotationPopupAction.java
new file mode 100644
index 0000000..bd07024
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/actions/AddAnnotationPopupAction.java
@@ -0,0 +1,124 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.actions;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.util.Locale;
+
+import javax.swing.AbstractAction;
+
+import com.intellij.DynamicBundle;
+import com.intellij.openapi.actionSystem.ActionUpdateThread;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.editor.Caret;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.ui.AnActionButton;
+import com.intellij.ui.popup.list.ListPopupImpl;
+import edu.kit.kastel.sdq.artemis4j.grading.penalty.MistakeType;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
+import org.jetbrains.annotations.NotNull;
+
+public class AddAnnotationPopupAction extends AnAction {
+ private static final Locale LOCALE = DynamicBundle.getLocale();
+
+ @Override
+ public @NotNull ActionUpdateThread getActionUpdateThread() {
+ return ActionUpdateThread.BGT;
+ }
+
+ @Override
+ public void update(@NotNull AnActionEvent e) {
+ Caret caret = e.getData(CommonDataKeys.CARET);
+
+ // if no exercise config is loaded, we cannot make annotations
+ // if there is no caret we can not sensibly display a popup
+ e.getPresentation()
+ .setEnabledAndVisible(caret != null && PluginState.getInstance().isAssessing());
+ }
+
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ Caret caret = e.getData(CommonDataKeys.CARET);
+
+ // if no exercise config is loaded, we cannot make annotations
+ // if there is no caret we can not sensibly display a popup
+ if (caret == null || !PluginState.getInstance().isAssessing()) {
+ return;
+ }
+
+ // collect all mistake types that can be annotated
+ var mistakeTypes = PluginState.getInstance()
+ .getActiveAssessment()
+ .orElseThrow()
+ .getGradingConfig()
+ .getMistakeTypes();
+
+ var actions = new DefaultActionGroup();
+ for (var mistakeType : mistakeTypes) {
+ actions.add(new MistakeTypeButton(mistakeType));
+ }
+
+ // create a popup with all possible mistakes
+ var popup = JBPopupFactory.getInstance()
+ .createActionGroupPopup(
+ "Add Annotation",
+ actions,
+ DataContext.EMPTY_CONTEXT,
+ JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
+ false);
+
+ // Code borrowed from ListPopupImpl#createContent (line 323) to allow ctrl+enter for selection
+ var listPopup = ((ListPopupImpl) popup);
+ listPopup.registerAction(
+ "handleSelectionCtrl", KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK, new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ popup.handleSelect(
+ true,
+ new KeyEvent(
+ listPopup.getList(),
+ KeyEvent.KEY_PRESSED,
+ e.getWhen(),
+ e.getModifiers(),
+ KeyEvent.VK_ENTER,
+ KeyEvent.CHAR_UNDEFINED));
+ }
+ });
+
+ popup.showInBestPositionFor(caret.getEditor());
+ }
+
+ private static class MistakeTypeButton extends AnActionButton {
+ private final MistakeType mistakeType;
+
+ public MistakeTypeButton(MistakeType mistakeType) {
+ super(mistakeType.getButtonText().translateTo(LOCALE));
+ this.mistakeType = mistakeType;
+ }
+
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ boolean withCustomMessage =
+ e.getInputEvent() != null && e.getInputEvent().isControlDown();
+ PluginState.getInstance()
+ .getActiveAssessment()
+ .orElseThrow()
+ .addAnnotationAtCaret(mistakeType, withCustomMessage);
+ }
+
+ @Override
+ public @NotNull ActionUpdateThread getActionUpdateThread() {
+ return ActionUpdateThread.EDT;
+ }
+
+ @Override
+ public boolean isDumbAware() {
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/autograder/AutograderTask.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/autograder/AutograderTask.java
new file mode 100644
index 0000000..9756d2e
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/autograder/AutograderTask.java
@@ -0,0 +1,103 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.autograder;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Locale;
+import java.util.function.Consumer;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.ui.Messages;
+import de.firemage.autograder.api.loader.AutograderLoader;
+import edu.kit.kastel.sdq.artemis4j.grading.Assessment;
+import edu.kit.kastel.sdq.artemis4j.grading.ClonedProgrammingSubmission;
+import edu.kit.kastel.sdq.artemis4j.grading.autograder.AutograderFailedException;
+import edu.kit.kastel.sdq.artemis4j.grading.autograder.AutograderRunner;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.AutograderOption;
+import edu.kit.kastel.sdq.intelligrade.utils.ArtemisUtils;
+import edu.kit.kastel.sdq.intelligrade.utils.IntellijUtil;
+import org.jetbrains.annotations.NotNull;
+
+public class AutograderTask extends Task.Backgroundable {
+ private static final Logger LOG = Logger.getInstance(AutograderTask.class);
+
+ private final Assessment assessment;
+ private final ClonedProgrammingSubmission clonedSubmission;
+ private final Runnable onSuccessCallback;
+
+ public static void execute(
+ Assessment assessment, ClonedProgrammingSubmission clonedSubmission, Runnable onSuccessCallback) {
+ new AutograderTask(assessment, clonedSubmission, onSuccessCallback)
+ .setCancelText("Stop Autograder")
+ .queue();
+ }
+
+ private AutograderTask(Assessment assessment, ClonedProgrammingSubmission clonedSubmission, Runnable onSuccess) {
+ super(IntellijUtil.getActiveProject(), "Autograder", true);
+
+ this.assessment = assessment;
+ this.clonedSubmission = clonedSubmission;
+ this.onSuccessCallback = onSuccess;
+ }
+
+ public void run(@NotNull ProgressIndicator indicator) {
+ indicator.setIndeterminate(true);
+
+ var settings = ArtemisSettingsState.getInstance();
+
+ // Load Autograder from file
+ if (settings.getAutograderOption() == AutograderOption.FROM_FILE
+ && !loadAutograderFromFile(settings, indicator)) {
+ return;
+ }
+
+ try {
+ Consumer statusConsumer = status -> indicator.setText("Autograder: " + status);
+
+ var stats = AutograderRunner.runAutograder(assessment, clonedSubmission, Locale.GERMANY, 2, statusConsumer);
+
+ String message = "Autograder made %d annotation(s). Please double-check all of them for false-positives!"
+ .formatted(stats.annotationsMade());
+ ApplicationManager.getApplication()
+ .invokeLater(
+ () -> Messages.showMessageDialog(message, "Autograder Completed", AllIcons.Status.Success));
+
+ ApplicationManager.getApplication().invokeLater(this.onSuccessCallback);
+ } catch (AutograderFailedException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon("Autograder Failed", e.getMessage());
+ }
+ }
+
+ private boolean loadAutograderFromFile(ArtemisSettingsState settings, ProgressIndicator indicator) {
+ if (!AutograderLoader.isAutograderLoaded()) {
+ var path = settings.getAutograderPath();
+ if (path == null || path.isBlank()) {
+ ArtemisUtils.displayGenericErrorBalloon(
+ "No Autograder Path",
+ "Please set the path to the Autograder JAR, or choose to download it from GitHub.");
+ return false;
+ }
+
+ indicator.setText("Loading Autograder");
+ try {
+ AutograderLoader.loadFromFile(Path.of(settings.getAutograderPath()));
+ } catch (IOException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon("Could not load Autograder", e.getMessage());
+ return false;
+ }
+ } else {
+ ArtemisUtils.displayGenericWarningBalloon(
+ "Autograder Already Loaded",
+ "Not reloading it from the specified file. Restart the IDE to reload it.");
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AnnotationsListPanel.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AnnotationsListPanel.java
new file mode 100644
index 0000000..ed753bf
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AnnotationsListPanel.java
@@ -0,0 +1,182 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.guis;
+
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import javax.swing.SwingUtilities;
+
+import com.intellij.openapi.actionSystem.ActionUpdateThread;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.ui.SimpleToolWindowPanel;
+import com.intellij.openapi.wm.ToolWindowManager;
+import com.intellij.ui.AnActionButton;
+import com.intellij.ui.PopupHandler;
+import com.intellij.ui.ScrollPaneFactory;
+import com.intellij.ui.table.JBTable;
+import edu.kit.kastel.sdq.artemis4j.grading.Annotation;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
+import edu.kit.kastel.sdq.intelligrade.utils.IntellijUtil;
+import org.jetbrains.annotations.NotNull;
+
+public class AnnotationsListPanel extends SimpleToolWindowPanel {
+ private final List displayAnnotations = new ArrayList<>();
+ private final AnnotationsTableModel model;
+ private final JBTable table;
+
+ public static AnnotationsListPanel getPanel() {
+ var toolWindow =
+ ToolWindowManager.getInstance(IntellijUtil.getActiveProject()).getToolWindow("Annotations");
+ return (AnnotationsListPanel)
+ toolWindow.getContentManager().getContent(0).getComponent();
+ }
+
+ public AnnotationsListPanel() {
+ super(true, true);
+
+ model = new AnnotationsTableModel();
+
+ table = new JBTable(model);
+ table.setAutoCreateRowSorter(true);
+
+ setContent(ScrollPaneFactory.createScrollPane(table));
+
+ // Delete annotation on delete key press
+ table.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyReleased(KeyEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_DELETE) {
+ // First collect all annotations to delete, then delete them
+ // If delete them one by one, the row indices change and the wrong annotations are deleted
+ var annotationsToDelete = Arrays.stream(table.getSelectedRows())
+ .map(table::convertRowIndexToModel)
+ .filter(row -> row >= 0)
+ .mapToObj(model::get)
+ .toList();
+
+ var assessment =
+ PluginState.getInstance().getActiveAssessment().orElseThrow();
+ for (var annotation : annotationsToDelete) {
+ assessment.deleteAnnotation(annotation);
+ }
+ }
+ }
+ });
+
+ // Double-clicks on the table
+ table.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ int row = table.rowAtPoint(e.getPoint());
+ int column = table.columnAtPoint(e.getPoint());
+
+ if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2 && row >= 0) {
+ var annotation = model.get(table.convertRowIndexToModel(row));
+
+ if (table.convertColumnIndexToModel(column) == AnnotationsTableModel.CUSTOM_MESSAGE_COLUMN) {
+ // Edit the custom message
+ PluginState.getInstance()
+ .getActiveAssessment()
+ .orElseThrow()
+ .changeCustomMessage(annotation);
+ } else {
+ // Jump to the line in the editor
+ var file = IntellijUtil.getAnnotationFile(annotation);
+ var document = FileDocumentManager.getInstance().getDocument(file);
+ int offset = document.getLineStartOffset(annotation.getStartLine());
+ FileEditorManager.getInstance(IntellijUtil.getActiveProject())
+ .openTextEditor(
+ new OpenFileDescriptor(IntellijUtil.getActiveProject(), file, offset), true);
+ }
+ }
+ }
+ });
+
+ // Add the right-click menu
+ addPopupMenu();
+
+ PluginState.getInstance()
+ .registerAssessmentStartedListener(
+ assessment -> assessment.registerAnnotationsUpdatedListener(annotations -> {
+ this.displayAnnotations.clear();
+ this.displayAnnotations.addAll(annotations);
+ this.displayAnnotations.sort(Comparator.comparing(Annotation::getFilePath)
+ .thenComparing(Annotation::getStartLine)
+ .thenComparing(Annotation::getEndLine));
+
+ AnnotationsTableModel tableModel = ((AnnotationsTableModel) table.getModel());
+ tableModel.setAnnotations(displayAnnotations);
+ table.revalidate();
+ table.updateUI();
+ }));
+
+ PluginState.getInstance().registerAssessmentClosedListener(() -> {
+ this.displayAnnotations.clear();
+ AnnotationsTableModel tableModel = ((AnnotationsTableModel) table.getModel());
+ tableModel.clearAnnotations();
+ table.updateUI();
+ });
+ }
+
+ public void selectAnnotation(Annotation annotation) {
+ var row = displayAnnotations.indexOf(annotation);
+ if (row >= 0) {
+ int viewRow = table.convertRowIndexToView(row);
+ table.setRowSelectionInterval(viewRow, viewRow);
+ }
+ }
+
+ private void addPopupMenu() {
+ var group = new DefaultActionGroup();
+
+ var editButton = new AnActionButton("Edit Custom Message/Score") {
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ var row = table.convertRowIndexToModel(table.getSelectedRow());
+ if (row >= 0) {
+ PluginState.getInstance()
+ .getActiveAssessment()
+ .orElseThrow()
+ .changeCustomMessage(model.get(row));
+ }
+ }
+
+ @Override
+ public @NotNull ActionUpdateThread getActionUpdateThread() {
+ return ActionUpdateThread.EDT;
+ }
+ };
+ group.addAction(editButton);
+
+ var deleteButton = new AnActionButton("Delete") {
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ var row = table.convertRowIndexToModel(table.getSelectedRow());
+ if (row >= 0) {
+ PluginState.getInstance()
+ .getActiveAssessment()
+ .orElseThrow()
+ .deleteAnnotation(model.get(row));
+ }
+ }
+
+ @Override
+ public @NotNull ActionUpdateThread getActionUpdateThread() {
+ return ActionUpdateThread.EDT;
+ }
+ };
+ group.addAction(deleteButton);
+
+ PopupHandler.installPopupMenu(table, group, "popup@AnnotationsListPanel");
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AnnotationsTableModel.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AnnotationsTableModel.java
new file mode 100644
index 0000000..eb605d8
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AnnotationsTableModel.java
@@ -0,0 +1,98 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.guis;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.intellij.DynamicBundle;
+import edu.kit.kastel.sdq.artemis4j.grading.Annotation;
+
+/**
+ * The table model for the annotations table.
+ */
+public class AnnotationsTableModel extends AbstractTableModel {
+
+ public static final int MISTAKE_TYPE_COLUMN = 0;
+ public static final int LINES_COLUMN = 1;
+ public static final int FILE_COLUMN = 2;
+ public static final int SOURCE_COLUMN = 3;
+ public static final int CUSTOM_MESSAGE_COLUMN = 4;
+ public static final int CUSTOM_PENALTY_COLUMN = 5;
+
+ private static final String[] HEADINGS = {
+ "Mistake type", "Line(s)", "File", "Source", "Custom Message", "Custom Penalty"
+ };
+
+ private static final Locale LOCALE = DynamicBundle.getLocale();
+
+ private final List annotations = new ArrayList<>();
+
+ @Override
+ public int getRowCount() {
+ return annotations.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return HEADINGS.length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ return HEADINGS[column];
+ }
+
+ @Override
+ public Object getValueAt(int row, int column) {
+ Annotation annotation = annotations.get(row);
+
+ if (annotation == null) {
+ return "";
+ }
+
+ return switch (column) {
+ case MISTAKE_TYPE_COLUMN -> annotation
+ .getMistakeType()
+ .getButtonText()
+ .translateTo(LOCALE);
+ case LINES_COLUMN -> formatLines(annotation);
+ case FILE_COLUMN -> annotation.getFilePath();
+ case SOURCE_COLUMN -> annotation.getSource();
+ case CUSTOM_MESSAGE_COLUMN -> annotation.getCustomMessage().orElse("");
+ case CUSTOM_PENALTY_COLUMN -> annotation
+ .getCustomScore()
+ .map(String::valueOf)
+ .orElse("");
+ default -> throw new IllegalStateException("No table data at index %d:%d".formatted(row, column));
+ };
+ }
+
+ public void setAnnotations(List annotations) {
+ this.annotations.clear();
+ this.annotations.addAll(annotations);
+ fireTableDataChanged();
+ }
+
+ public void clearAnnotations() {
+ this.annotations.clear();
+ fireTableDataChanged();
+ }
+
+ public Annotation get(int index) {
+ return annotations.get(index);
+ }
+
+ private String formatLines(Annotation annotation) {
+ int startLine = annotation.getStartLine() + 1;
+ int endLine = annotation.getEndLine() + 1;
+
+ if (startLine == endLine) {
+ return String.valueOf(startLine);
+ } else {
+ return String.format("%d - %d", startLine, endLine);
+ }
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AssessmentPanel.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AssessmentPanel.java
new file mode 100644
index 0000000..792feb4
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/AssessmentPanel.java
@@ -0,0 +1,261 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.guis;
+
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.font.TextAttribute;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.swing.*;
+import javax.swing.border.TitledBorder;
+import javax.swing.plaf.LayerUI;
+
+import com.intellij.DynamicBundle;
+import com.intellij.openapi.ui.SimpleToolWindowPanel;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.ScrollPaneFactory;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBPanel;
+import com.intellij.util.ui.JBFont;
+import edu.kit.kastel.sdq.artemis4j.grading.Assessment;
+import edu.kit.kastel.sdq.artemis4j.grading.penalty.*;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import edu.kit.kastel.sdq.intelligrade.state.ActiveAssessment;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
+import net.miginfocom.swing.MigLayout;
+
+public class AssessmentPanel extends SimpleToolWindowPanel {
+ private static final Locale LOCALE = DynamicBundle.getLocale();
+
+ private final JPanel content;
+ private final JBLabel pointsLabel;
+ private final Map ratingGroupBorders = new IdentityHashMap<>();
+ private final List assessmentButtons = new ArrayList<>();
+
+ public AssessmentPanel() {
+ super(true, true);
+
+ content = new JBPanel<>(new MigLayout("wrap 1", "[grow]"));
+ setContent(ScrollPaneFactory.createScrollPane(content));
+
+ pointsLabel = new JBLabel();
+
+ this.showNoActiveAssessment();
+ PluginState.getInstance().registerAssessmentStartedListener(assessment -> {
+ content.removeAll();
+
+ content.add(pointsLabel, "alignx center");
+
+ var infoLabel = new JBLabel("Hold Ctrl to add a custom message");
+ infoLabel.setForeground(JBColor.GRAY);
+ content.add(infoLabel, "alignx center");
+
+ assessment.registerAnnotationsUpdatedListener(annotations -> {
+ var a = assessment.getAssessment();
+ var testPoints = a.calculateTotalPointsOfTests();
+ var annotationPoints = a.calculateTotalPointsOfAnnotations();
+ var totalPoints = a.calculateTotalPoints();
+ var maxPoints = a.getMaxPoints();
+ pointsLabel.setText(getAssessmentPointsTitle(testPoints, annotationPoints, totalPoints, maxPoints));
+ });
+
+ this.createMistakeButtons(assessment);
+ });
+ PluginState.getInstance().registerAssessmentClosedListener(this::showNoActiveAssessment);
+ }
+
+ private void createMistakeButtons(ActiveAssessment assessment) {
+ int buttonsPerRow = ArtemisSettingsState.getInstance().getColumnsPerRatingGroup();
+ for (var ratingGroup : assessment.getGradingConfig().getRatingGroups()) {
+ if (ratingGroup.getMistakeTypes().isEmpty()) {
+ continue;
+ }
+
+ var panel = new JBPanel<>(new MigLayout("fill, gap 0, wrap " + buttonsPerRow));
+
+ var border = BorderFactory.createTitledBorder(
+ BorderFactory.createLineBorder(JBColor.border()),
+ String.format(getRatingGroupTitle(assessment.getAssessment(), ratingGroup)));
+ border.setTitleFont(JBFont.h3().asBold());
+ panel.setBorder(border);
+ this.ratingGroupBorders.put(ratingGroup, border);
+
+ for (var mistakeType : ratingGroup.getMistakeTypes()) {
+ var button = new JButton(mistakeType.getButtonText().translateTo(LOCALE));
+ button.setToolTipText(mistakeType.getMessage().translateTo(LOCALE));
+
+ var iconRenderer = new MistakeTypeIconRenderer();
+ var layer = new LayerUI<>() {
+ @Override
+ public void paint(Graphics g, JComponent c) {
+ super.paint(g, c);
+ iconRenderer.paint((Graphics2D) g, c);
+ }
+ };
+ JPanel buttonPanel = new JPanel(new MigLayout("fill"));
+ buttonPanel.add(button, "growx");
+ panel.add(new JLayer<>(buttonPanel, layer), "growx, sizegroup main");
+
+ button.addActionListener(a -> assessment.addAnnotationAtCaret(
+ mistakeType, (a.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK));
+ this.assessmentButtons.add(new AssessmentButton(mistakeType, button, iconRenderer));
+ }
+
+ this.content.add(panel, "growx");
+ }
+
+ assessment.registerAnnotationsUpdatedListener(annotations -> {
+ var a = assessment.getAssessment();
+ updateRatingGroupTitles(a);
+ updateButtonIcons(a);
+ });
+
+ this.updateUI();
+ }
+
+ private void updateRatingGroupTitles(Assessment assessment) {
+ for (var entry : this.ratingGroupBorders.entrySet()) {
+ var ratingGroup = entry.getKey();
+ var border = entry.getValue();
+ border.setTitle(getRatingGroupTitle(assessment, ratingGroup));
+ }
+ }
+
+ private void updateButtonIcons(Assessment assessment) {
+ for (AssessmentButton assessmentButton : this.assessmentButtons) {
+ var settings = ArtemisSettingsState.getInstance();
+ var mistakeType = assessmentButton.mistakeType();
+
+ String iconText;
+ Color color;
+ Font font = JBFont.regular();
+
+ if (mistakeType.getReporting().shouldScore()) {
+ int count = assessment.getAnnotations(mistakeType).size();
+ var rule = mistakeType.getRule();
+
+ switch (rule) {
+ case ThresholdPenaltyRule thresholdRule -> {
+ iconText = count + "/" + thresholdRule.getThreshold();
+ if (count >= thresholdRule.getThreshold()) {
+ color = settings.getFinishedAssessmentButtonColor();
+ font = font.deriveFont(Map.of(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON));
+ } else {
+ color = settings.getActiveAssessmentButtonColor();
+ }
+ }
+ case CustomPenaltyRule customPenaltyRule -> {
+ iconText = "C";
+ color = settings.getActiveAssessmentButtonColor();
+ }
+ case StackingPenaltyRule stackingPenaltyRule -> {
+ iconText = String.valueOf(count);
+ color = settings.getActiveAssessmentButtonColor();
+ }
+ }
+ } else {
+ iconText = "R";
+ color = settings.getReportingAssessmentButtonColor();
+ }
+
+ assessmentButton.iconRenderer().update(iconText, color);
+ assessmentButton.button().setForeground(color);
+ assessmentButton.button().setFont(font);
+ }
+ }
+
+ private void showNoActiveAssessment() {
+ this.ratingGroupBorders.clear();
+ this.assessmentButtons.clear();
+
+ content.removeAll();
+ content.add(new JBLabel("No active assessment"), "growx");
+ this.updateUI();
+ }
+
+ private String getRatingGroupTitle(Assessment assessment, RatingGroup ratingGroup) {
+ var points = assessment.calculatePointsForRatingGroup(ratingGroup);
+ return "%s (%.1f of [%.1f, %.1f])"
+ .formatted(
+ ratingGroup.getDisplayName().translateTo(LOCALE),
+ points.score(),
+ ratingGroup.getMinPenalty(),
+ ratingGroup.getMaxPenalty());
+ }
+
+ private static String getAssessmentPointsTitle(
+ double testPoints, double annotationPoints, double totalPoints, double maxPoints) {
+ if (annotationPoints > 0.0) {
+ return """
+ %.1f %.1f = %.1f of %.1f
"""
+ .formatted(
+ colorToCSS(JBColor.GREEN),
+ testPoints,
+ colorToCSS(JBColor.GREEN),
+ Math.abs(annotationPoints),
+ totalPoints,
+ maxPoints);
+ } else {
+ return """
+ %.1f - %.1f = %.1f of %.1f
"""
+ .formatted(
+ colorToCSS(JBColor.GREEN),
+ testPoints,
+ colorToCSS(JBColor.RED),
+ Math.abs(annotationPoints),
+ totalPoints,
+ maxPoints);
+ }
+ }
+
+ private static String colorToCSS(JBColor color) {
+ return "rgb(%d, %d, %d)".formatted(color.getRed(), color.getGreen(), color.getBlue());
+ }
+
+ private static class MistakeTypeIconRenderer {
+ private final JBFont font;
+
+ private String text;
+ private Color bgColor;
+
+ private int textWidth;
+ private int baselineHeight;
+ private int textHeight;
+
+ public MistakeTypeIconRenderer() {
+ this.font = JBFont.regular();
+ this.update("", JBColor.foreground());
+ }
+
+ public void paint(Graphics2D g, JComponent component) {
+ g.setFont(this.font);
+
+ if (textWidth < 0) {
+ textWidth = g.getFontMetrics().stringWidth(text);
+ baselineHeight = g.getFontMetrics().getMaxAscent();
+ textHeight =
+ g.getFontMetrics().getMaxAscent() + g.getFontMetrics().getMaxDescent();
+ }
+
+ g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+ g.setColor(bgColor);
+ g.fillRoundRect(component.getWidth() - textWidth - 5, 0, textWidth + 2, textHeight, 2, 2);
+
+ g.setColor(JBColor.background());
+ g.drawString(text, component.getWidth() - textWidth - 4, baselineHeight);
+ }
+
+ private void update(String text, Color bgColor) {
+ this.text = text;
+ this.bgColor = bgColor;
+ this.textWidth = -1;
+ }
+ }
+
+ private record AssessmentButton(MistakeType mistakeType, JButton button, MistakeTypeIconRenderer iconRenderer) {}
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/ExercisePanel.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/ExercisePanel.java
new file mode 100644
index 0000000..1f2a5c6
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/ExercisePanel.java
@@ -0,0 +1,471 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.guis;
+
+import java.awt.event.ItemEvent;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import javax.swing.event.DocumentEvent;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.openapi.ui.MessageDialogBuilder;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.SimpleToolWindowPanel;
+import com.intellij.openapi.ui.TextBrowseFolderListener;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.wm.ToolWindowManager;
+import com.intellij.ui.DocumentAdapter;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.ScrollPaneFactory;
+import com.intellij.ui.TitledSeparator;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBPanel;
+import com.intellij.ui.components.JBTextField;
+import com.intellij.ui.components.TextComponentEmptyText;
+import edu.kit.kastel.sdq.artemis4j.ArtemisNetworkException;
+import edu.kit.kastel.sdq.artemis4j.client.AssessmentStatsDTO;
+import edu.kit.kastel.sdq.artemis4j.client.AssessmentType;
+import edu.kit.kastel.sdq.artemis4j.grading.ArtemisConnection;
+import edu.kit.kastel.sdq.artemis4j.grading.Course;
+import edu.kit.kastel.sdq.artemis4j.grading.Exam;
+import edu.kit.kastel.sdq.artemis4j.grading.ProgrammingExercise;
+import edu.kit.kastel.sdq.artemis4j.grading.ProgrammingSubmission;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import edu.kit.kastel.sdq.intelligrade.state.ActiveAssessment;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
+import edu.kit.kastel.sdq.intelligrade.utils.ArtemisUtils;
+import edu.kit.kastel.sdq.intelligrade.utils.IntellijUtil;
+import net.miginfocom.swing.MigLayout;
+import org.jetbrains.annotations.NotNull;
+
+public class ExercisePanel extends SimpleToolWindowPanel {
+ private static final Logger LOG = Logger.getInstance(ExercisePanel.class);
+
+ private final JBLabel connectedLabel;
+
+ private final ComboBox courseSelector;
+ private final ComboBox examSelector;
+ private final ComboBox exerciseSelector;
+
+ private JPanel generalPanel;
+ private JButton startGradingRound1Button;
+ private JButton startGradingRound2Button;
+ private TextFieldWithBrowseButton gradingConfigPathInput;
+
+ private JPanel statisticsPanel;
+ private JBLabel totalStatisticsLabel;
+ private JBLabel userStatisticsLabel;
+
+ private JPanel assessmentPanel;
+ private JButton submitAssessmentButton;
+ private JButton cancelAssessmentButton;
+ private JButton saveAssessmentButton;
+ private JButton closeAssessmentButton;
+ private JButton reRunAutograder;
+
+ private JPanel backlogPanel;
+ private JPanel backlogList;
+
+ public ExercisePanel() {
+ super(true, true);
+
+ connectedLabel = new JBLabel();
+ JPanel content = new JBPanel<>(new MigLayout("wrap 2", "[][grow]"));
+ content.add(connectedLabel, "span 2, alignx center");
+
+ content.add(new JBLabel("Course:"));
+ courseSelector = new ComboBox<>();
+ content.add(courseSelector, "growx");
+
+ content.add(new JBLabel("Exam:"));
+ examSelector = new ComboBox<>();
+ content.add(examSelector, "growx");
+
+ content.add(new JBLabel("Exercise:"));
+ exerciseSelector = new ComboBox<>();
+ content.add(exerciseSelector, "growx");
+
+ createStatisticsPanel();
+ content.add(statisticsPanel, "span 2, growx");
+
+ createGeneralPanel();
+ content.add(new TitledSeparator("General"), "spanx 2, growx");
+ content.add(generalPanel, "span 2, growx");
+
+ content.add(new TitledSeparator("Assessment"), "spanx 2, growx");
+ createAssessmentPanel();
+ content.add(assessmentPanel, "span 2, growx");
+
+ content.add(new TitledSeparator("Backlog"), "spanx 2, growx");
+ createBacklogPanel();
+ content.add(backlogPanel, "span 2, growx");
+
+ setContent(ScrollPaneFactory.createScrollPane(content));
+
+ exerciseSelector.addItemListener(this::handleExerciseSelected);
+
+ examSelector.addItemListener(this::handleExamSelected);
+
+ courseSelector.addItemListener(this::handleCourseSelected);
+
+ PluginState.getInstance().registerConnectedListener(this::handleConnectionChange);
+
+ PluginState.getInstance().registerAssessmentStartedListener(this::handleAssessmentStarted);
+
+ PluginState.getInstance().registerAssessmentClosedListener(this::handleAssessmentClosed);
+ }
+
+ private void createGeneralPanel() {
+ generalPanel = new JBPanel<>(new MigLayout("wrap 1", "[grow]"));
+
+ startGradingRound1Button = new JButton("Start Grading Round 1");
+ startGradingRound1Button.setForeground(JBColor.GREEN);
+ startGradingRound1Button.addActionListener(
+ a -> PluginState.getInstance().startNextAssessment(0));
+ generalPanel.add(startGradingRound1Button, "growx");
+
+ startGradingRound2Button = new JButton("Start Grading Round 2");
+ startGradingRound2Button.setForeground(JBColor.GREEN);
+ startGradingRound2Button.addActionListener(
+ a -> PluginState.getInstance().startNextAssessment(1));
+ generalPanel.add(startGradingRound2Button, "growx");
+
+ gradingConfigPathInput = new TextFieldWithBrowseButton();
+ gradingConfigPathInput.addBrowseFolderListener(
+ new TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor("json")));
+ gradingConfigPathInput.setText(ArtemisSettingsState.getInstance().getSelectedGradingConfigPath());
+ gradingConfigPathInput.getTextField().getDocument().addDocumentListener(new DocumentAdapter() {
+ @Override
+ protected void textChanged(@NotNull DocumentEvent documentEvent) {
+ ArtemisSettingsState.getInstance().setSelectedGradingConfigPath(gradingConfigPathInput.getText());
+ }
+ });
+ generalPanel.add(gradingConfigPathInput, "growx");
+
+ var innerTextField = (JBTextField) gradingConfigPathInput.getTextField();
+ innerTextField.getEmptyText().setText("Path to grading config");
+ innerTextField.putClientProperty(TextComponentEmptyText.STATUS_VISIBLE_FUNCTION, (Predicate)
+ f -> f.getText().isEmpty());
+ }
+
+ private void createStatisticsPanel() {
+ statisticsPanel = new JBPanel<>(new MigLayout("wrap 2", "[][grow]"));
+
+ statisticsPanel.add(new JBLabel("Submissions:"));
+ totalStatisticsLabel = new JBLabel();
+ statisticsPanel.add(totalStatisticsLabel);
+
+ statisticsPanel.add(new JBLabel("Your Assessments:"));
+ userStatisticsLabel = new JBLabel();
+ statisticsPanel.add(userStatisticsLabel);
+ }
+
+ private void createAssessmentPanel() {
+ assessmentPanel = new JBPanel<>(new MigLayout("wrap 2", "[grow][grow]"));
+ assessmentPanel.setEnabled(false);
+
+ submitAssessmentButton = new JButton("Submit Assessment");
+ submitAssessmentButton.setForeground(JBColor.GREEN);
+ submitAssessmentButton.setEnabled(false);
+ submitAssessmentButton.addActionListener(a -> PluginState.getInstance().submitAssessment());
+ assessmentPanel.add(submitAssessmentButton, "growx");
+
+ cancelAssessmentButton = new JButton("Cancel Assessment");
+ cancelAssessmentButton.setEnabled(false);
+ cancelAssessmentButton.addActionListener(a -> {
+ var confirmed = MessageDialogBuilder.okCancel(
+ "Cancel Assessment?", "Your assessment will be discarded, and the lock will be freed.")
+ .guessWindowAndAsk();
+
+ if (confirmed) {
+ PluginState.getInstance().cancelAssessment();
+ }
+ });
+ assessmentPanel.add(cancelAssessmentButton, "growx");
+
+ saveAssessmentButton = new JButton("Save Assessment");
+
+ saveAssessmentButton.setEnabled(false);
+ saveAssessmentButton.addActionListener(a -> PluginState.getInstance().saveAssessment());
+ assessmentPanel.add(saveAssessmentButton, "growx");
+
+ closeAssessmentButton = new JButton("Close Assessment");
+ closeAssessmentButton.setEnabled(false);
+ closeAssessmentButton.addActionListener(a -> {
+ var confirmed = MessageDialogBuilder.okCancel(
+ "Close Assessment?", "Your will loose any unsaved progress, but you will keep the lock.")
+ .guessWindowAndAsk();
+
+ if (confirmed) {
+ PluginState.getInstance().closeAssessment();
+ }
+ });
+ assessmentPanel.add(closeAssessmentButton, "growx");
+
+ reRunAutograder = new JButton("Re-run Autograder");
+ reRunAutograder.setEnabled(false);
+ reRunAutograder.addActionListener(a -> {
+ var confirmed = MessageDialogBuilder.okCancel(
+ "Re-Run Autograder?", "This may create duplicate annotations!")
+ .guessWindowAndAsk();
+
+ if (confirmed) {
+ PluginState.getInstance().getActiveAssessment().orElseThrow().runAutograder();
+ }
+ });
+ assessmentPanel.add(reRunAutograder, "spanx 2, growx");
+ }
+
+ private void createBacklogPanel() {
+ backlogPanel = new JBPanel<>(new MigLayout("wrap 2", "[grow] []"));
+
+ backlogList = new JBPanel<>(new MigLayout("wrap 4, gapx 30", "[][][][grow]"));
+ backlogPanel.add(ScrollPaneFactory.createScrollPane(backlogList, true), "spanx 2, growx");
+
+ var refreshButton = new JButton(AllIcons.Actions.Refresh);
+ refreshButton.addActionListener(a -> updateBacklogAndStats());
+ backlogPanel.add(refreshButton, "skip 1, alignx right");
+ }
+
+ private void handleExerciseSelected(ItemEvent e) {
+ // Exercise selected: Update plugin state, enable/disable grading buttons, update backlog
+ if (e.getStateChange() != ItemEvent.DESELECTED) {
+ var exercise = (ProgrammingExercise) e.getItem();
+ startGradingRound2Button.setEnabled(
+ !PluginState.getInstance().isAssessing() && exercise.hasSecondCorrectionRound());
+
+ PluginState.getInstance().setActiveExercise(exercise);
+
+ updateBacklogAndStats();
+ }
+ }
+
+ private void handleExamSelected(ItemEvent e) {
+ // If an exam was selected, update the exercise selector with the exercises of the exam
+ // If no exam was selected, update the exercise selector with the exercises of the course
+ if (e.getStateChange() == ItemEvent.DESELECTED) {
+ return;
+ }
+
+ try {
+ exerciseSelector.removeAllItems();
+ var item = (OptionalExam) e.getItem();
+ if (item.exam() != null) {
+ for (var group : item.exam().getExerciseGroups()) {
+ for (var exercise : group.getProgrammingExercises()) {
+ exerciseSelector.addItem(exercise);
+ }
+ }
+ } else {
+ for (ProgrammingExercise programmingExercise :
+ courseSelector.getItem().getProgrammingExercises()) {
+ exerciseSelector.addItem(programmingExercise);
+ }
+ }
+ } catch (ArtemisNetworkException ex) {
+ LOG.warn(ex);
+ ArtemisUtils.displayNetworkErrorBalloon("Failed to fetch exercise info", ex);
+ }
+
+ updateUI();
+ }
+
+ private void handleCourseSelected(ItemEvent e) {
+ // Course was selected: Update the exam selector with the exams of the course
+ // This triggers an item event in the exam selector, which updates the exercise selector
+ if (e.getStateChange() != ItemEvent.DESELECTED) {
+ try {
+ var course = (Course) e.getItem();
+ examSelector.removeAllItems();
+ examSelector.addItem(new OptionalExam(null));
+ for (Exam exam : course.getExams()) {
+ examSelector.addItem(new OptionalExam(exam));
+ }
+ updateUI();
+ } catch (ArtemisNetworkException ex) {
+ LOG.warn(ex);
+ ArtemisUtils.displayNetworkErrorBalloon("Failed to fetch exam info", ex);
+ }
+ }
+ }
+
+ private void handleConnectionChange(Optional connection) {
+ courseSelector.removeAllItems();
+
+ if (connection.isPresent()) {
+ // When a connection is established, update the course selector with the courses of the connection
+ try {
+ connectedLabel.setText("✔ Connected to "
+ + connection.get().getClient().getInstance().getDomain() + " as "
+ + connection.get().getAssessor().getLogin());
+ connectedLabel.setForeground(JBColor.GREEN);
+ for (Course course : connection.get().getCourses()) {
+ courseSelector.addItem(course);
+ }
+ } catch (ArtemisNetworkException ex) {
+ LOG.warn(ex);
+ ArtemisUtils.displayNetworkErrorBalloon("Failed to fetch course info", ex);
+ }
+ } else {
+ connectedLabel.setText("❌ Not connected");
+ connectedLabel.setForeground(JBColor.RED);
+ }
+
+ updateUI();
+ }
+
+ private void handleAssessmentStarted(ActiveAssessment assessment) {
+ startGradingRound1Button.setEnabled(false);
+ startGradingRound2Button.setEnabled(false);
+
+ assessmentPanel.setEnabled(true);
+ submitAssessmentButton.setEnabled(true);
+ cancelAssessmentButton.setEnabled(
+ !assessment.getAssessment().getSubmission().isSubmitted());
+ saveAssessmentButton.setEnabled(true);
+ closeAssessmentButton.setEnabled(true);
+ reRunAutograder.setEnabled(true);
+
+ updateBacklogAndStats();
+ }
+
+ private void handleAssessmentClosed() {
+ startGradingRound1Button.setEnabled(true);
+ startGradingRound2Button.setEnabled(exerciseSelector.getItem().hasSecondCorrectionRound());
+
+ assessmentPanel.setEnabled(false);
+ submitAssessmentButton.setEnabled(false);
+ cancelAssessmentButton.setEnabled(false);
+ saveAssessmentButton.setEnabled(false);
+ closeAssessmentButton.setEnabled(false);
+ reRunAutograder.setEnabled(false);
+
+ updateBacklogAndStats();
+ }
+
+ private void updateBacklogAndStats() {
+ // Fetch data in the background, but do all UI updates on the EDT!
+ ApplicationManager.getApplication().executeOnPooledThread(() -> {
+ var exercise = PluginState.getInstance().getActiveExercise().orElseThrow();
+
+ List submissions;
+ AssessmentStatsDTO stats;
+ try {
+ submissions = exercise.fetchSubmissions();
+ stats = exercise.fetchAssessmentStats();
+ } catch (ArtemisNetworkException ex) {
+ LOG.warn(ex);
+ ArtemisUtils.displayNetworkErrorBalloon("Failed to fetch backlog or statistics", ex);
+ ApplicationManager.getApplication().invokeLater(() -> {
+ backlogList.removeAll();
+ this.updateUI();
+ });
+ return;
+ }
+
+ ApplicationManager.getApplication().invokeLater(() -> {
+ updateStatistics(exercise, stats, submissions);
+ updateBacklog(submissions);
+ updateUI();
+
+ // Tell the user that we've done something
+ ToolWindowManager.getInstance(IntellijUtil.getActiveProject())
+ .notifyByBalloon("Artemis", MessageType.INFO, "Backlog updated");
+ });
+ });
+ }
+
+ private void updateStatistics(
+ ProgrammingExercise exercise, AssessmentStatsDTO stats, List submissions) {
+ String totalText;
+ if (exercise.hasSecondCorrectionRound()) {
+ totalText = "%d / %d / %d (%d locked)"
+ .formatted(
+ stats.numberOfAssessmentsOfCorrectionRounds()
+ .getFirst()
+ .inTime(),
+ stats.numberOfAssessmentsOfCorrectionRounds().get(1).inTime(),
+ stats.numberOfSubmissions().inTime(),
+ stats.totalNumberOfAssessmentLocks());
+ } else {
+ totalText = "%d / %d (%d locked)"
+ .formatted(
+ stats.numberOfAssessmentsOfCorrectionRounds()
+ .getFirst()
+ .inTime(),
+ stats.numberOfSubmissions().inTime(),
+ stats.totalNumberOfAssessmentLocks());
+ }
+ totalStatisticsLabel.setText(totalText);
+
+ int submittedSubmissions = (int)
+ submissions.stream().filter(ProgrammingSubmission::isSubmitted).count();
+ int lockedSubmissions = (int)
+ submissions.stream().filter(ExercisePanel::isSubmissionStarted).count();
+ String userText = "%d (%d locked)".formatted(submittedSubmissions, lockedSubmissions);
+ userStatisticsLabel.setText(userText);
+ }
+
+ private void updateBacklog(List submissions) {
+ backlogList.removeAll();
+
+ List sortedSubmissions = new ArrayList<>(submissions);
+ sortedSubmissions.sort(Comparator.comparing(ProgrammingSubmission::getSubmissionDate));
+ for (ProgrammingSubmission submission : sortedSubmissions) {
+ String dateText = submission
+ .getSubmissionDate()
+ .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT));
+ backlogList.add(new JBLabel(dateText), "alignx right");
+
+ // Correction Round
+ backlogList.add(new JBLabel("Round " + (submission.getCorrectionRound() + 1)));
+
+ // Score in percent
+ var latestResult = submission.getLatestResult();
+ String resultText = "";
+ if (submission.isSubmitted()) {
+ resultText = latestResult
+ .map(resultDTO -> "%.0f%%".formatted(resultDTO.score()))
+ .orElse("???");
+ }
+ backlogList.add(new JBLabel(resultText), "alignx right");
+
+ // Action Button
+ JButton reopenButton;
+ if (submission.isSubmitted()) {
+ reopenButton = new JButton("Reopen Assessment");
+ } else if (isSubmissionStarted(submission)) {
+ reopenButton = new JButton("Continue Assessment");
+ reopenButton.setForeground(JBColor.ORANGE);
+ } else {
+ reopenButton = new JButton("Start Assessment");
+ }
+ reopenButton.addActionListener(a -> PluginState.getInstance().reopenAssessment(submission));
+ backlogList.add(reopenButton, "growx");
+ }
+ }
+
+ private static boolean isSubmissionStarted(ProgrammingSubmission submission) {
+ return !submission.isSubmitted()
+ && submission.getLatestResult().isPresent()
+ && submission.getLatestResult().get().assessmentType() != AssessmentType.AUTOMATIC;
+ }
+
+ private record OptionalExam(Exam exam) {
+ @Override
+ public String toString() {
+ return exam == null ? "" : exam.toString();
+ }
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/TestCasePanel.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/TestCasePanel.java
new file mode 100644
index 0000000..58d90b1
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/guis/TestCasePanel.java
@@ -0,0 +1,55 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.guis;
+
+import javax.swing.JPanel;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.ui.SimpleToolWindowPanel;
+import com.intellij.ui.ScrollPaneFactory;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBPanel;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
+import net.miginfocom.swing.MigLayout;
+
+public class TestCasePanel extends SimpleToolWindowPanel {
+ private final JPanel content;
+
+ public TestCasePanel() {
+ super(true, true);
+
+ this.content = new JBPanel<>(new MigLayout("wrap 3, gapx 10px, gapy 5px", "[][][]"));
+
+ setContent(ScrollPaneFactory.createScrollPane(content));
+
+ PluginState.getInstance().registerAssessmentStartedListener(assessment -> {
+ this.content.removeAll();
+
+ var testResults = assessment.getAssessment().getTestResults();
+ for (var result : testResults) {
+ String tooltip = result.getDetailText().orElse("No details available");
+
+ var icon = result.getPoints() != 0.0
+ ? AllIcons.RunConfigurations.TestPassed
+ : AllIcons.RunConfigurations.TestFailed;
+ var iconLabel = new JBLabel(icon);
+ iconLabel.setToolTipText(tooltip);
+ this.content.add(iconLabel);
+
+ var testName = new JBLabel(result.getTestName());
+ testName.setToolTipText(tooltip);
+ this.content.add(testName);
+
+ String points = result.getPoints() != 0.0 ? String.format("%.3fP", result.getPoints()) : "";
+ this.content.add(new JBLabel(points));
+ }
+
+ updateUI();
+ });
+
+ PluginState.getInstance().registerAssessmentClosedListener(() -> {
+ this.content.removeAll();
+ this.content.add(new JBLabel("No active assessment"), "spanx 3, alignx center");
+ updateUI();
+ });
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisCredentialsProvider.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisCredentialsProvider.java
new file mode 100644
index 0000000..a464f0b
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisCredentialsProvider.java
@@ -0,0 +1,75 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.settings;
+
+import com.intellij.credentialStore.CredentialAttributes;
+import com.intellij.credentialStore.CredentialAttributesKt;
+import com.intellij.ide.passwordSafe.PasswordSafe;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.Service;
+
+@Service
+public final class ArtemisCredentialsProvider {
+ private static final String CREDENTIALS_PATH = "edu.kit.kastel.intelligrade.artemisCredentials";
+ private static final String PASSWORD_STORE_KEY = "artemisPassword";
+ private static final String JWT_STORE_KEY = "artemisAuthJWT";
+
+ private boolean initialized = false;
+ private String artemisPassword;
+ private String jwt;
+
+ public static ArtemisCredentialsProvider getInstance() {
+ return ApplicationManager.getApplication().getService(ArtemisCredentialsProvider.class);
+ }
+
+ public void initialize() {
+ // Prefetches the credentials, since the password store is very slow
+ ApplicationManager.getApplication().executeOnPooledThread(() -> {
+ var safe = PasswordSafe.getInstance();
+ artemisPassword = safe.getPassword(createCredentialAttributes(PASSWORD_STORE_KEY));
+ jwt = safe.getPassword(createCredentialAttributes(JWT_STORE_KEY));
+ synchronized (this) {
+ initialized = true;
+ this.notifyAll();
+ }
+ });
+ }
+
+ public String getArtemisPassword() {
+ waitForInitialization();
+ return artemisPassword;
+ }
+
+ public void setArtemisPassword(String artemisPassword) {
+ this.artemisPassword = artemisPassword;
+ ApplicationManager.getApplication().executeOnPooledThread(() -> PasswordSafe.getInstance()
+ .setPassword(createCredentialAttributes(PASSWORD_STORE_KEY), artemisPassword));
+ }
+
+ public String getJwt() {
+ waitForInitialization();
+ return jwt;
+ }
+
+ public void setJwt(String jwt) {
+ this.jwt = jwt;
+ ApplicationManager.getApplication().executeOnPooledThread(() -> PasswordSafe.getInstance()
+ .setPassword(createCredentialAttributes(JWT_STORE_KEY), jwt));
+ }
+
+ private CredentialAttributes createCredentialAttributes(String key) {
+ return new CredentialAttributes(CredentialAttributesKt.generateServiceName(CREDENTIALS_PATH, key));
+ }
+
+ private void waitForInitialization() {
+ synchronized (this) {
+ while (!initialized) {
+ try {
+ this.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisSettings.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisSettings.java
new file mode 100644
index 0000000..dea187e
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisSettings.java
@@ -0,0 +1,307 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.settings;
+
+import java.util.Objects;
+
+import javax.swing.*;
+
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.options.Configurable;
+import com.intellij.openapi.ui.TextBrowseFolderListener;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.util.NlsContexts;
+import com.intellij.ui.ColorPanel;
+import com.intellij.ui.JBIntSpinner;
+import com.intellij.ui.TitledSeparator;
+import com.intellij.ui.components.JBCheckBox;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBPanel;
+import com.intellij.ui.components.JBPasswordField;
+import com.intellij.ui.components.JBRadioButton;
+import com.intellij.ui.components.JBTextField;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
+import net.miginfocom.swing.MigLayout;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This class implements the settings Dialog for this PlugIn.
+ * Everything directly related to the Setting UI should be in here.
+ */
+public class ArtemisSettings implements Configurable {
+ private JBTextField artemisURLField;
+
+ private JBLabel usernameLabel;
+ private JBRadioButton useTokenLoginButton;
+ private JBLabel passwordLabel;
+ private JBRadioButton usePasswordLoginButton;
+ private JBTextField usernameField;
+ private JBPasswordField passwordField;
+ private JBRadioButton useVcsSSH;
+ private JBRadioButton useVcsToken;
+
+ private JBRadioButton autograderDownloadButton;
+ private JBRadioButton autograderPathButton;
+ private TextFieldWithBrowseButton autograderPathField;
+ private JBRadioButton autograderSkipButton;
+
+ private JBCheckBox autoOpenMainClassCheckBox;
+ private JBIntSpinner columnsPerRatingGroupSpinner;
+ private ColorPanel highlighterColorChooser;
+ private ColorPanel activeAssessmentButtonColorChooser;
+ private ColorPanel finishedAssessmentButtonColorChooser;
+ private ColorPanel reportingAssessmentButtonColorChooser;
+
+ /**
+ * Returns the visible name of the configurable component.
+ * Note, that this method must return the display name
+ * that is equal to the display name declared in XML
+ * to avoid unexpected errors.
+ *
+ * @return the visible name of the configurable component
+ */
+ @Override
+ public @NlsContexts.ConfigurableName String getDisplayName() {
+ return "Artemis (IntelliGrade)";
+ }
+
+ /**
+ * Creates a new Swing form that enables the user to configure the settings.
+ * Usually this method is called on the EDT, so it should not take a long time.
+ * Also, this place is designed to allocate resources (subscriptions/listeners etc.)
+ *
+ * @return new Swing form to show, or {@code null} if it cannot be created
+ * @see #disposeUIResources
+ */
+ @Override
+ public @Nullable JComponent createComponent() {
+ var contentPanel = new JBPanel<>(new MigLayout("wrap 2", "[] [grow]"));
+
+ contentPanel.add(new JBLabel("Artemis URL:"));
+ artemisURLField = new JBTextField();
+ contentPanel.add(artemisURLField, "growx");
+
+ var loginButton = new JButton("(Re-)Connect");
+ loginButton.addActionListener(a -> {
+ ArtemisCredentialsProvider.getInstance().setJwt(null);
+ ArtemisSettingsState.getInstance().setJwtExpiry(null);
+ this.apply();
+ PluginState.getInstance().connect();
+ });
+ contentPanel.add(loginButton, "span 2, growx");
+
+ // Login options
+ contentPanel.add(new TitledSeparator("Login Options"), "span 2, growx");
+ var loginButtonGroup = new ButtonGroup();
+
+ useTokenLoginButton = new JBRadioButton("Token Login (Preferred)");
+ useTokenLoginButton.addActionListener(a -> updateLoginType());
+ loginButtonGroup.add(useTokenLoginButton);
+ contentPanel.add(useTokenLoginButton, "span 2, growx");
+
+ usePasswordLoginButton = new JBRadioButton("Password Login");
+ usePasswordLoginButton.addActionListener(a -> updateLoginType());
+ loginButtonGroup.add(usePasswordLoginButton);
+ contentPanel.add(usePasswordLoginButton, "span 2, growx");
+
+ usernameLabel = new JBLabel("Username:");
+ contentPanel.add(usernameLabel, "pad 0 40 0 0, growx");
+ usernameField = new JBTextField();
+ contentPanel.add(usernameField, "growx");
+
+ passwordLabel = new JBLabel("Password:");
+ contentPanel.add(passwordLabel, "pad 0 40 0 0, growx");
+ passwordField = new JBPasswordField();
+ contentPanel.add(passwordField, "growx");
+
+ // VCS Access
+ contentPanel.add(new TitledSeparator("VCS Access"), "span 2, grow x");
+ ButtonGroup vcsAccessButtonGroup = new ButtonGroup();
+
+ useVcsToken = new JBRadioButton("VCS Token");
+ contentPanel.add(useVcsToken);
+ vcsAccessButtonGroup.add(useVcsToken);
+
+ useVcsSSH = new JBRadioButton("SSH");
+ contentPanel.add(useVcsSSH);
+ vcsAccessButtonGroup.add(useVcsSSH);
+
+ // Autograder options
+ contentPanel.add(new TitledSeparator("Autograder"), "span 2, growx");
+ ButtonGroup autograderButtonGroup = new ButtonGroup();
+
+ autograderDownloadButton = new JBRadioButton("Download latest Autograder release from GitHub");
+ autograderDownloadButton.addActionListener(a -> updateAutograderOptions());
+ autograderButtonGroup.add(autograderDownloadButton);
+ contentPanel.add(autograderDownloadButton, "span 2, growx");
+
+ autograderPathButton = new JBRadioButton("Use local Autograder JAR");
+ autograderPathButton.addActionListener(a -> updateAutograderOptions());
+ autograderButtonGroup.add(autograderPathButton);
+ contentPanel.add(autograderPathButton, "span 2, growx");
+ autograderPathField = new TextFieldWithBrowseButton();
+ var fileDescriptor =
+ new TextBrowseFolderListener(new FileChooserDescriptor(true, false, true, true, false, false)
+ .withFileFilter(file -> "jar".equalsIgnoreCase(file.getExtension())));
+ autograderPathField.addBrowseFolderListener(fileDescriptor);
+ contentPanel.add(autograderPathField, "pad 0 40 0 0, span 2, growx");
+
+ autograderSkipButton = new JBRadioButton("Skip Autograder");
+ autograderSkipButton.addActionListener(a -> updateAutograderOptions());
+ autograderButtonGroup.add(autograderSkipButton);
+ contentPanel.add(autograderSkipButton, "span 2, growx");
+
+ // UI / General options
+ contentPanel.add(new TitledSeparator("General"), "span 2, growx");
+ autoOpenMainClassCheckBox = new JBCheckBox("Auto-open main class");
+ contentPanel.add(autoOpenMainClassCheckBox, "span 2, growx");
+
+ contentPanel.add(new JBLabel("Columns per rating group:"));
+ columnsPerRatingGroupSpinner = new JBIntSpinner(3, 1, 50);
+ contentPanel.add(columnsPerRatingGroupSpinner, "growx");
+
+ contentPanel.add(new JBLabel("Highlighter color:"));
+ highlighterColorChooser = new ColorPanel();
+ contentPanel.add(highlighterColorChooser, "growx");
+
+ contentPanel.add(new JBLabel("Scoring grading button:"));
+ activeAssessmentButtonColorChooser = new ColorPanel();
+ contentPanel.add(activeAssessmentButtonColorChooser, "growx");
+
+ contentPanel.add(new JBLabel("Scoring grading button (limit reached):"));
+ finishedAssessmentButtonColorChooser = new ColorPanel();
+ contentPanel.add(finishedAssessmentButtonColorChooser, "growx");
+
+ contentPanel.add(new JBLabel("Reporting grading button:"));
+ reportingAssessmentButtonColorChooser = new ColorPanel();
+ contentPanel.add(reportingAssessmentButtonColorChooser, "growx");
+
+ return contentPanel;
+ }
+
+ /**
+ * Indicates whether the Swing form was modified or not.
+ * This method is called very often, so it should not take a long time.
+ *
+ * @return {@code true} if the settings were modified, {@code false} otherwise
+ */
+ @Override
+ public boolean isModified() {
+ var settings = ArtemisSettingsState.getInstance();
+ var credentials = ArtemisCredentialsProvider.getInstance();
+
+ boolean modified = !new String(passwordField.getPassword()).equals(credentials.getArtemisPassword());
+ modified |= !usernameField.getText().equals(settings.getUsername());
+ modified |= !artemisURLField.getText().equals(settings.getArtemisInstanceUrl());
+ modified |= !columnsPerRatingGroupSpinner.getValue().equals(settings.getColumnsPerRatingGroup());
+ modified |= !Objects.equals(highlighterColorChooser.getSelectedColor(), settings.getAnnotationColor());
+ modified |= useTokenLoginButton.isSelected() != settings.isUseTokenLogin();
+ modified |= getSelectedAutograderOption() != settings.getAutograderOption();
+ modified |= autoOpenMainClassCheckBox.isSelected() != settings.isAutoOpenMainClass();
+ modified |= getSelectedVcsOption() != settings.getVcsAccessOption();
+ modified |= !Objects.equals(
+ activeAssessmentButtonColorChooser.getSelectedColor(), settings.getActiveAssessmentButtonColor());
+ modified |= !Objects.equals(
+ finishedAssessmentButtonColorChooser.getSelectedColor(), settings.getFinishedAssessmentButtonColor());
+ modified |= !Objects.equals(
+ reportingAssessmentButtonColorChooser.getSelectedColor(), settings.getReportingAssessmentButtonColor());
+ return modified;
+ }
+
+ /**
+ * Stores the settings from the Swing form to the configurable component.
+ * This method is called on EDT upon user's request.
+ */
+ @Override
+ public void apply() {
+ var settings = ArtemisSettingsState.getInstance();
+ var credentials = ArtemisCredentialsProvider.getInstance();
+
+ settings.setArtemisInstanceUrl(artemisURLField.getText());
+
+ settings.setUseTokenLogin(useTokenLoginButton.isSelected());
+ settings.setUsername(usernameField.getText());
+ credentials.setArtemisPassword(new String(passwordField.getPassword()));
+
+ settings.setVcsAccessOption(getSelectedVcsOption());
+
+ settings.setAutograderOption(getSelectedAutograderOption());
+ settings.setAutograderPath(autograderPathField.getText());
+
+ settings.setAutoOpenMainClass(autoOpenMainClassCheckBox.isSelected());
+ settings.setColumnsPerRatingGroup(
+ Integer.parseInt(columnsPerRatingGroupSpinner.getValue().toString()));
+ settings.setAnnotationColor(highlighterColorChooser.getSelectedColor());
+ settings.setActiveAssessmentButtonColor(activeAssessmentButtonColorChooser.getSelectedColor());
+ settings.setFinishedAssessmentButtonColor(finishedAssessmentButtonColorChooser.getSelectedColor());
+ settings.setReportingAssessmentButtonColor(reportingAssessmentButtonColorChooser.getSelectedColor());
+ }
+
+ /**
+ * Loads the settings from the configurable component to the Swing form.
+ * This method is called on EDT immediately after the form creation or later upon user's request.
+ */
+ @Override
+ public void reset() {
+ var settings = ArtemisSettingsState.getInstance();
+ var credentials = ArtemisCredentialsProvider.getInstance();
+
+ artemisURLField.setText(settings.getArtemisInstanceUrl());
+
+ useTokenLoginButton.setSelected(settings.isUseTokenLogin());
+ usePasswordLoginButton.setSelected(!settings.isUseTokenLogin());
+ usernameField.setText(settings.getUsername());
+ passwordField.setText(credentials.getArtemisPassword());
+
+ switch (settings.getVcsAccessOption()) {
+ case SSH -> useVcsSSH.setSelected(true);
+ case TOKEN -> useVcsToken.setSelected(true);
+ }
+
+ switch (settings.getAutograderOption()) {
+ case FROM_GITHUB -> autograderDownloadButton.setSelected(true);
+ case FROM_FILE -> autograderPathButton.setSelected(true);
+ case SKIP -> autograderSkipButton.setSelected(true);
+ }
+ autograderPathField.setText(settings.getAutograderPath());
+
+ autoOpenMainClassCheckBox.setSelected(settings.isAutoOpenMainClass());
+ columnsPerRatingGroupSpinner.setValue(settings.getColumnsPerRatingGroup());
+ highlighterColorChooser.setSelectedColor(settings.getAnnotationColor());
+ activeAssessmentButtonColorChooser.setSelectedColor(settings.getActiveAssessmentButtonColor());
+ finishedAssessmentButtonColorChooser.setSelectedColor(settings.getFinishedAssessmentButtonColor());
+ reportingAssessmentButtonColorChooser.setSelectedColor(settings.getReportingAssessmentButtonColor());
+
+ updateLoginType();
+ updateAutograderOptions();
+ }
+
+ private void updateLoginType() {
+ var useToken = useTokenLoginButton.isSelected();
+ usernameLabel.setEnabled(!useToken);
+ passwordLabel.setEnabled(!useToken);
+ usernameField.setEnabled(!useToken);
+ passwordField.setEnabled(!useToken);
+ }
+
+ private void updateAutograderOptions() {
+ autograderPathField.setEnabled(autograderPathButton.isSelected());
+ }
+
+ private AutograderOption getSelectedAutograderOption() {
+ if (autograderDownloadButton.isSelected()) {
+ return AutograderOption.FROM_GITHUB;
+ } else if (autograderPathButton.isSelected()) {
+ return AutograderOption.FROM_FILE;
+ } else {
+ return AutograderOption.SKIP;
+ }
+ }
+
+ private VCSAccessOption getSelectedVcsOption() {
+ if (useVcsSSH.isSelected()) {
+ return VCSAccessOption.SSH;
+ } else {
+ return VCSAccessOption.TOKEN;
+ }
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisSettingsState.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisSettingsState.java
new file mode 100644
index 0000000..35526d3
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/ArtemisSettingsState.java
@@ -0,0 +1,192 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.settings;
+
+import java.awt.*;
+import java.util.Date;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.ui.JBColor;
+import com.intellij.util.xmlb.XmlSerializer;
+import com.intellij.util.xmlb.XmlSerializerUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This class persists all required data for the PlugIn.
+ * Secrets (such as the Artemis password) are handled by the IntelliJ secrets provider
+ */
+@State(name = "edu.kit.kastel.extensions.ArtemisSettingsState", storages = @Storage("IntelliGradeSettings.xml"))
+public class ArtemisSettingsState implements PersistentStateComponent {
+ private final InternalState state = new InternalState();
+
+ // Settings need to be public for IntelliJ to serialize them
+ @SuppressWarnings("java:S1104")
+ public static class InternalState {
+ public boolean useTokenLogin = true;
+ public VCSAccessOption vcsAccessOption = VCSAccessOption.TOKEN;
+ public String username = "";
+ public String artemisInstanceUrl = "";
+ public AutograderOption autograderOption = AutograderOption.FROM_GITHUB;
+ public String autograderPath = null;
+ public boolean autoOpenMainClass = true;
+ public String selectedGradingConfigPath;
+ public int columnsPerRatingGroup = 3;
+
+ public Date jwtExpiry = new Date(Long.MAX_VALUE);
+
+ public int annotationColor = new JBColor(new Color(155, 54, 54), new Color(155, 54, 54)).getRGB();
+ public int activeAssessmentButtonColor = JBColor.YELLOW.getRGB();
+ public int finishedAssessmentButtonColor = JBColor.MAGENTA.getRGB();
+ public int reportingAssessmentButtonColor = JBColor.GREEN.getRGB();
+ }
+
+ public static ArtemisSettingsState getInstance() {
+ return ApplicationManager.getApplication().getService(ArtemisSettingsState.class);
+ }
+
+ /**
+ * Gets the Settings state.
+ *
+ * @return state.a component state.
+ * All properties, public and annotated fields are serialized.
+ * Only values which differ from the default (i.e. the value of newly instantiated class)
+ * are serialized. {@code null} value indicates that the returned state won't be stored,
+ * as a result previously stored state will be used.
+ * @see XmlSerializer
+ */
+ @Override
+ public @Nullable InternalState getState() {
+ return state;
+ }
+
+ /**
+ * This method is called when a new component state is loaded.
+ * The method can and will be called several times if config
+ * files are externally changed while the IDE is running.
+ * State object should be used directly, defensive copying is not required.
+ *
+ * @param state loaded component state
+ * @see XmlSerializerUtil#copyBean(Object, Object)
+ */
+ @Override
+ public void loadState(@NotNull InternalState state) {
+ XmlSerializerUtil.copyBean(state, this.state);
+ }
+
+ public boolean isUseTokenLogin() {
+ return state.useTokenLogin;
+ }
+
+ public void setUseTokenLogin(boolean useTokenLogin) {
+ state.useTokenLogin = useTokenLogin;
+ }
+
+ public String getUsername() {
+ return state.username;
+ }
+
+ public void setUsername(String username) {
+ state.username = username;
+ }
+
+ public String getArtemisInstanceUrl() {
+ return state.artemisInstanceUrl;
+ }
+
+ public void setArtemisInstanceUrl(String artemisInstanceUrl) {
+ // invalidate JWT if URL changed
+ ArtemisCredentialsProvider.getInstance().setJwt("");
+ state.artemisInstanceUrl = artemisInstanceUrl;
+ }
+
+ public @Nullable String getSelectedGradingConfigPath() {
+ return state.selectedGradingConfigPath;
+ }
+
+ public void setSelectedGradingConfigPath(@Nullable String selectedGradingConfigPath) {
+ state.selectedGradingConfigPath = selectedGradingConfigPath;
+ }
+
+ public int getColumnsPerRatingGroup() {
+ return state.columnsPerRatingGroup;
+ }
+
+ public void setColumnsPerRatingGroup(int columnsPerRatingGroup) {
+ state.columnsPerRatingGroup = columnsPerRatingGroup;
+ }
+
+ public Color getAnnotationColor() {
+ return new Color(state.annotationColor);
+ }
+
+ public void setAnnotationColor(Color annotationColor) {
+ state.annotationColor = annotationColor.getRGB();
+ }
+
+ public Date getJwtExpiry() {
+ return state.jwtExpiry;
+ }
+
+ public void setJwtExpiry(Date jwtExpiry) {
+ state.jwtExpiry = jwtExpiry;
+ }
+
+ public AutograderOption getAutograderOption() {
+ return state.autograderOption;
+ }
+
+ public void setAutograderOption(AutograderOption autograderOption) {
+ state.autograderOption = autograderOption;
+ }
+
+ public String getAutograderPath() {
+ return state.autograderPath;
+ }
+
+ public void setAutograderPath(String autograderPath) {
+ state.autograderPath = autograderPath;
+ }
+
+ public boolean isAutoOpenMainClass() {
+ return state.autoOpenMainClass;
+ }
+
+ public void setAutoOpenMainClass(boolean autoOpenMainClass) {
+ state.autoOpenMainClass = autoOpenMainClass;
+ }
+
+ public VCSAccessOption getVcsAccessOption() {
+ return state.vcsAccessOption;
+ }
+
+ public void setVcsAccessOption(VCSAccessOption vcsAccessOption) {
+ state.vcsAccessOption = vcsAccessOption;
+ }
+
+ public Color getActiveAssessmentButtonColor() {
+ return new Color(state.activeAssessmentButtonColor);
+ }
+
+ public void setActiveAssessmentButtonColor(Color activeAssessmentButtonColor) {
+ state.activeAssessmentButtonColor = activeAssessmentButtonColor.getRGB();
+ }
+
+ public Color getFinishedAssessmentButtonColor() {
+ return new Color(state.finishedAssessmentButtonColor);
+ }
+
+ public void setFinishedAssessmentButtonColor(Color finishedAssessmentButtonColor) {
+ state.finishedAssessmentButtonColor = finishedAssessmentButtonColor.getRGB();
+ }
+
+ public Color getReportingAssessmentButtonColor() {
+ return new Color(state.reportingAssessmentButtonColor);
+ }
+
+ public void setReportingAssessmentButtonColor(Color reportingAssessmentButtonColor) {
+ state.reportingAssessmentButtonColor = reportingAssessmentButtonColor.getRGB();
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/AutograderOption.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/AutograderOption.java
new file mode 100644
index 0000000..2b3c04a
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/AutograderOption.java
@@ -0,0 +1,8 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.settings;
+
+public enum AutograderOption {
+ SKIP,
+ FROM_GITHUB,
+ FROM_FILE
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/VCSAccessOption.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/VCSAccessOption.java
new file mode 100644
index 0000000..ad6f6f4
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/settings/VCSAccessOption.java
@@ -0,0 +1,7 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.settings;
+
+public enum VCSAccessOption {
+ SSH,
+ TOKEN
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/tool_windows/AnnotationsToolWindowFactory.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/tool_windows/AnnotationsToolWindowFactory.java
new file mode 100644
index 0000000..01a5a21
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/tool_windows/AnnotationsToolWindowFactory.java
@@ -0,0 +1,22 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.tool_windows;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowFactory;
+import com.intellij.ui.content.ContentFactory;
+import edu.kit.kastel.sdq.intelligrade.extensions.guis.AnnotationsListPanel;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This class generates the tool Window for annotations in the bottom.
+ */
+public class AnnotationsToolWindowFactory implements ToolWindowFactory {
+
+ @Override
+ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
+ var content = ContentFactory.getInstance().createContent(new AnnotationsListPanel(), null, false);
+ toolWindow.show();
+ toolWindow.getContentManager().addContent(content);
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/tool_windows/MainToolWindowFactory.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/tool_windows/MainToolWindowFactory.java
new file mode 100644
index 0000000..cbaecb3
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/extensions/tool_windows/MainToolWindowFactory.java
@@ -0,0 +1,31 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.extensions.tool_windows;
+
+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.content.ContentFactory;
+import edu.kit.kastel.sdq.intelligrade.extensions.guis.AssessmentPanel;
+import edu.kit.kastel.sdq.intelligrade.extensions.guis.ExercisePanel;
+import edu.kit.kastel.sdq.intelligrade.extensions.guis.TestCasePanel;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This class handles all logic for the main grading UI.
+ * It does not handle any other logic, that should be factored out.
+ */
+public class MainToolWindowFactory implements ToolWindowFactory, DumbAware {
+ @Override
+ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
+ toolWindow
+ .getContentManager()
+ .addContent(ContentFactory.getInstance().createContent(new ExercisePanel(), "Exercise", false));
+ toolWindow
+ .getContentManager()
+ .addContent(ContentFactory.getInstance().createContent(new AssessmentPanel(), "Grading", false));
+ toolWindow
+ .getContentManager()
+ .addContent(ContentFactory.getInstance().createContent(new TestCasePanel(), "Test Results", false));
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/highlighter/HighlighterManager.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/highlighter/HighlighterManager.java
new file mode 100644
index 0000000..ffe85f5
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/highlighter/HighlighterManager.java
@@ -0,0 +1,291 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.highlighter;
+
+import java.awt.Font;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.swing.Icon;
+
+import com.intellij.DynamicBundle;
+import com.intellij.openapi.actionSystem.ActionGroup;
+import com.intellij.openapi.actionSystem.ActionUpdateThread;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.event.EditorMouseEvent;
+import com.intellij.openapi.editor.markup.EffectType;
+import com.intellij.openapi.editor.markup.GutterIconRenderer;
+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.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.FileEditorManagerListener;
+import com.intellij.openapi.fileEditor.TextEditor;
+import com.intellij.openapi.ui.popup.JBPopup;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.AnActionButton;
+import com.intellij.ui.JBColor;
+import edu.kit.kastel.sdq.artemis4j.grading.Annotation;
+import edu.kit.kastel.sdq.intelligrade.extensions.guis.AnnotationsListPanel;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import edu.kit.kastel.sdq.intelligrade.icons.ArtemisIcons;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
+import edu.kit.kastel.sdq.intelligrade.utils.IntellijUtil;
+import org.jetbrains.annotations.NotNull;
+
+public class HighlighterManager {
+ private static final Map> highlightersPerEditor = new IdentityHashMap<>();
+
+ // private static int lastPopupLine;
+ // private static Editor lastPopupEditor;
+ private static JBPopup lastPopup;
+
+ public static void initialize() {
+ var messageBus = IntellijUtil.getActiveProject().getMessageBus();
+ messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerListener() {
+ @Override
+ public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
+ var editor = source.getSelectedTextEditor();
+
+ if (PluginState.getInstance().isAssessing()) {
+ editor.getDocument().setReadOnly(true);
+ updateHighlightersForEditor(editor);
+ }
+ }
+
+ @Override
+ public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
+ var editor = source.getSelectedTextEditor();
+ if (editor == null) {
+ return;
+ }
+
+ clearHighlightersForEditor(editor);
+ }
+ });
+
+ PluginState.getInstance()
+ .registerAssessmentStartedListener(assessment -> assessment.registerAnnotationsUpdatedListener(
+ annotations -> updateHighlightersForAllEditors()));
+
+ // When an assessment is closed, clear everything
+ PluginState.getInstance().registerAssessmentClosedListener(() -> {
+ highlightersPerEditor.clear();
+ cancelLastPopup();
+ });
+ }
+
+ public static void onMouseMovedInEditor(EditorMouseEvent e) {
+ // TODO Later implement feature
+ // var highlighters = highlightersPerEditor.get(e.getEditor());
+ // if (highlighters == null) {
+ // return;
+ // }
+ //
+ // int line = e.getLogicalPosition().getLine();
+ //
+ // // If the cursor is still in the same line, nothing has to change
+ // if (line == lastPopupLine && e.getEditor() == lastPopupEditor) {
+ // return;
+ // }
+ //
+ // var annotations = highlighters.stream().filter(h -> h.annotation().getStartLine() <= line &&
+ // h.annotation().getEndLine() >= line)
+ // .map(HighlighterWithAnnotation::annotation)
+ // .toList();
+ //
+ // if (!annotations.isEmpty()) {
+ // lastPopupLine = line;
+ // lastPopupEditor = e.getEditor();
+ //
+ // // First finish the current event, then show the popup
+ // // Otherwise, the event may be cancelled, and e.g. the caret not moved
+ // ApplicationManager.getApplication().invokeLater(() -> {
+ // lastPopup = JBPopupFactory.getInstance()
+ // .createPopupChooserBuilder(annotations)
+ // .setRenderer((list, annotation, index, isSelected, cellHasFocus) ->
+ // new
+ // JBLabel(annotation.getMistakeType().getButtonText().translateTo(DynamicBundle.getLocale())))
+ // .setModalContext(false)
+ // .setResizable(true)
+ // .setRequestFocus(false)
+ // .setCancelOnClickOutside(false)
+ // .createPopup();
+ //
+ // var point = e.getMouseEvent().getPoint();
+ // // point.translate(30, 10);
+ // lastPopup.show(new RelativePoint(e.getMouseEvent().getComponent(), point));
+ // }, x -> lastPopupLine != line || lastPopupEditor != e.getEditor() || lastPopup != null);
+ // } else {
+ // cancelLastPopup();
+ // }
+ }
+
+ private static void createHighlighter(Editor editor, int startLine, List annotations) {
+ var document = FileDocumentManager.getInstance().getDocument(editor.getVirtualFile());
+
+ int startOffset = document.getLineStartOffset(startLine);
+ int endOffset = document.getLineEndOffset(
+ annotations.stream().mapToInt(Annotation::getEndLine).max().orElse(startLine));
+
+ var annotationColor = ArtemisSettingsState.getInstance().getAnnotationColor();
+ var attributes = new TextAttributes(
+ null, new JBColor(annotationColor, annotationColor), null, EffectType.BOLD_LINE_UNDERSCORE, Font.PLAIN);
+
+ var highlighter = editor.getMarkupModel()
+ .addRangeHighlighter(
+ startOffset,
+ endOffset,
+ HighlighterLayer.SELECTION - 1,
+ attributes,
+ HighlighterTargetArea.LINES_IN_RANGE);
+
+ String gutterTooltip = annotations.stream()
+ .map(a -> {
+ String text = ""
+ + a.getMistakeType().getButtonText().translateTo(DynamicBundle.getLocale()) + "";
+ if (a.getCustomMessage().isPresent()) {
+ text += " " + a.getCustomMessage().get();
+ }
+
+ if (a.getCustomScore().isPresent()) {
+ text += " (" + a.getCustomScore().get() + ")";
+ }
+
+ return text;
+ })
+ .collect(Collectors.joining("
"));
+
+ var popupActions = getGutterPopupActions(annotations);
+
+ highlighter.setGutterIconRenderer(new GutterIconRenderer() {
+ @Override
+ public boolean equals(Object o) {
+ // TODO implement some actually useful equals method
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return 0;
+ }
+
+ @Override
+ public @NotNull Icon getIcon() {
+ return switch (annotations.size()) {
+ case 1 -> ArtemisIcons.AnnotationsGutter1;
+ case 2 -> ArtemisIcons.AnnotationsGutter2;
+ case 3 -> ArtemisIcons.AnnotationsGutter3;
+ default -> ArtemisIcons.AnnotationsGutter4;
+ };
+ }
+
+ @Override
+ public String getTooltipText() {
+ return gutterTooltip;
+ }
+
+ @Override
+ public ActionGroup getPopupMenuActions() {
+ return popupActions;
+ }
+
+ @Override
+ public boolean isDumbAware() {
+ return true;
+ }
+ });
+
+ highlightersPerEditor.computeIfAbsent(editor, e -> new ArrayList<>());
+ highlightersPerEditor.get(editor).add(new HighlighterWithAnnotations(highlighter, annotations));
+ }
+
+ private static void cancelLastPopup() {
+ // if (lastPopup != null) {
+ // if (!lastPopup.isDisposed()) {
+ // lastPopup.cancel();
+ // }
+ // lastPopup = null;
+ // lastPopupLine = -1;
+ // lastPopupEditor = null;
+ // }
+ }
+
+ private static void updateHighlightersForAllEditors() {
+ if (!PluginState.getInstance().isAssessing()) {
+ return;
+ }
+
+ var editors =
+ FileEditorManager.getInstance(IntellijUtil.getActiveProject()).getAllEditors();
+ for (var editor : editors) {
+ updateHighlightersForEditor(((TextEditor) editor).getEditor());
+ }
+
+ cancelLastPopup();
+ }
+
+ private static void updateHighlightersForEditor(Editor editor) {
+ // E.g. decompiled classes are not in the local file system
+ // Since they are never part of an assessment, ignore them
+ if (!editor.getVirtualFile().isInLocalFileSystem()) {
+ return;
+ }
+
+ clearHighlightersForEditor(editor);
+
+ var filePath = editor.getVirtualFile().toNioPath();
+ var state = PluginState.getInstance();
+ var assessment = state.getActiveAssessment().orElseThrow().getAssessment();
+ var annotationsByLine = assessment.getAnnotations().stream()
+ .filter(a -> IntellijUtil.getAnnotationPath(a).equals(filePath))
+ .collect(Collectors.groupingBy(Annotation::getStartLine));
+ for (var entry : annotationsByLine.entrySet()) {
+ createHighlighter(editor, entry.getKey(), entry.getValue());
+ }
+ }
+
+ private static void clearHighlightersForEditor(Editor editor) {
+ highlightersPerEditor.remove(editor);
+ editor.getMarkupModel().removeAllHighlighters();
+ }
+
+ private static ActionGroup getGutterPopupActions(List annotations) {
+ var group = new DefaultActionGroup();
+ for (Annotation annotation : annotations) {
+ String text = annotation.getMistakeType().getButtonText().translateTo(DynamicBundle.getLocale());
+ if (annotation.getCustomMessage().isPresent()) {
+ String displayPath = StringUtil.shortenPathWithEllipsis(
+ annotation.getCustomMessage().get(), 80);
+ text += ": " + displayPath;
+ }
+
+ group.addAction(new AnActionButton(text) {
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
+ AnnotationsListPanel.getPanel().selectAnnotation(annotation);
+ }
+
+ @Override
+ public boolean isDumbAware() {
+ return true;
+ }
+
+ @Override
+ public @NotNull ActionUpdateThread getActionUpdateThread() {
+ return ActionUpdateThread.EDT;
+ }
+ });
+ }
+ return group;
+ }
+
+ private record HighlighterWithAnnotations(RangeHighlighter highlighter, List annotation) {}
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/highlighter/WrapperPanel.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/highlighter/WrapperPanel.java
new file mode 100644
index 0000000..1cff72b
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/highlighter/WrapperPanel.java
@@ -0,0 +1,36 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.highlighter;
+
+import java.awt.*;
+
+import javax.swing.*;
+
+import com.intellij.ui.WidthBasedLayout;
+
+final class WrapperPanel extends JPanel implements WidthBasedLayout {
+
+ WrapperPanel(JComponent content) {
+ super(new BorderLayout());
+ setBorder(null);
+ setContent(content);
+ }
+
+ void setContent(JComponent content) {
+ removeAll();
+ add(content, BorderLayout.CENTER);
+ }
+
+ private JComponent getComponent() {
+ return (JComponent) getComponent(0);
+ }
+
+ @Override
+ public int getPreferredWidth() {
+ return WidthBasedLayout.getPreferredWidth(getComponent());
+ }
+
+ @Override
+ public int getPreferredHeight(int width) {
+ return WidthBasedLayout.getPreferredHeight(getComponent(), width);
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/icons/ArtemisIcons.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/icons/ArtemisIcons.java
new file mode 100644
index 0000000..55d21d1
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/icons/ArtemisIcons.java
@@ -0,0 +1,18 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.icons;
+
+import javax.swing.Icon;
+
+import com.intellij.openapi.util.IconLoader;
+
+// This class follows IntelliJ's code style conventions
+public final class ArtemisIcons {
+ private ArtemisIcons() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ public static final Icon AnnotationsGutter1 = IconLoader.getIcon("/icons/annotation1.svg", ArtemisIcons.class);
+ public static final Icon AnnotationsGutter2 = IconLoader.getIcon("/icons/annotation2.svg", ArtemisIcons.class);
+ public static final Icon AnnotationsGutter3 = IconLoader.getIcon("/icons/annotation3.svg", ArtemisIcons.class);
+ public static final Icon AnnotationsGutter4 = IconLoader.getIcon("/icons/annotation4.svg", ArtemisIcons.class);
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/FileOpener.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/FileOpener.java
new file mode 100644
index 0000000..c20f23b
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/FileOpener.java
@@ -0,0 +1,142 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.listeners;
+
+import com.intellij.ide.projectView.ProjectView;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.Service;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiArrayType;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.PsiTypes;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.GlobalSearchScopes;
+import com.intellij.psi.search.PsiShortNamesCache;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import edu.kit.kastel.sdq.intelligrade.state.ActiveAssessment;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
+import edu.kit.kastel.sdq.intelligrade.utils.IntellijUtil;
+
+@Service
+public final class FileOpener implements DumbService.DumbModeListener {
+ private static final Logger LOG = Logger.getInstance(FileOpener.class);
+
+ private volatile boolean openClassesNextTime = false;
+
+ public static FileOpener getInstance() {
+ return ApplicationManager.getApplication().getService(FileOpener.class);
+ }
+
+ public FileOpener() {
+ PluginState.getInstance().registerAssessmentStartedListener(a -> {
+ var settings = ArtemisSettingsState.getInstance();
+ synchronized (this) {
+ openClassesNextTime = settings.isAutoOpenMainClass();
+ }
+ });
+
+ PluginState.getInstance().registerAssessmentClosedListener(() -> {
+ // Relevant if building indices is not finished before the assessment is closed
+ synchronized (this) {
+ openClassesNextTime = false;
+ }
+ });
+ }
+
+ @Override
+ public void exitDumbMode() {
+ if (!openClassesNextTime || !PluginState.getInstance().isAssessing()) {
+ return;
+ }
+
+ // Open the main class
+ // Do this in the background because it may cause a synchronous vfs refresh
+ ApplicationManager.getApplication().executeOnPooledThread(() -> {
+ synchronized (this) {
+ if (!openClassesNextTime || !PluginState.getInstance().isAssessing()) {
+ return;
+ }
+
+ openClassesNextTime = false;
+ }
+
+ var project = IntellijUtil.getActiveProject();
+
+ // Only look in assignment/, we aren't interested in test classes
+ var directory = VfsUtil.findFile(
+ IntellijUtil.getProjectRootDirectory().resolve(ActiveAssessment.ASSIGNMENT_SUB_PATH), true);
+
+ if (directory == null) {
+ LOG.warn("Can't resolve assignment directory");
+ return;
+ }
+
+ // Even though we exited dumb mode, the index operations below may throw IndexNotReadyExceptions,
+ // so defensively wrap this in a smart mode action
+ DumbService.getInstance(project).runReadActionInSmartMode(() -> findAnOpenMainMethod(project, directory));
+ });
+ }
+
+ private static void findAnOpenMainMethod(Project project, VirtualFile directory) {
+ var scope = GlobalSearchScopes.directoryScope(project, directory, true);
+ var mainMethods = PsiShortNamesCache.getInstance(project).getMethodsByName("main", scope);
+
+ PsiType stringType =
+ PsiType.getJavaLangString(PsiManager.getInstance(project), GlobalSearchScope.allScope(project));
+ for (var method : mainMethods) {
+ // Is public & static
+ var modifiers = method.getModifierList();
+ if (!modifiers.hasExplicitModifier(PsiModifier.PUBLIC)
+ || !modifiers.hasExplicitModifier(PsiModifier.STATIC)) {
+ continue;
+ }
+
+ // Returns void
+ if (!PsiTypes.voidType().equals(method.getReturnType())) {
+ continue;
+ }
+
+ // Single parameter of type String[] or String...
+ var parameters = method.getParameterList();
+ if (parameters.getParametersCount() != 1) {
+ continue;
+ }
+
+ var parameter = parameters.getParameters()[0];
+ var type = parameter.getType();
+
+ // This should also cover varargs, since PsiEllipsisType is a subtype of PsiArrayType
+ if (type instanceof PsiArrayType arrayType) {
+ if (!stringType.equals(arrayType.getComponentType())) {
+ continue;
+ }
+ } else {
+ continue;
+ }
+
+ // All checks passed, this is a main method!
+ var file = method.getContainingFile().getVirtualFile();
+ int offset = method.getTextOffset();
+
+ ApplicationManager.getApplication().invokeLater(() -> {
+ // Open the file in an editor, and place the caret at the main method's declaration
+ FileEditorManager.getInstance(project)
+ .openTextEditor(new OpenFileDescriptor(project, file, offset), true);
+
+ // Expand the project view and select the file
+ ProjectView.getInstance(IntellijUtil.getActiveProject()).select(null, file, true);
+ });
+
+ return;
+ }
+
+ LOG.info("No main class found");
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/listeners/GradingConfigSelectedListener.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/GradingConfigSelectedListener.java
similarity index 68%
rename from src/main/java/edu/kit/kastel/listeners/GradingConfigSelectedListener.java
rename to src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/GradingConfigSelectedListener.java
index 281020b..4106205 100644
--- a/src/main/java/edu/kit/kastel/listeners/GradingConfigSelectedListener.java
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/GradingConfigSelectedListener.java
@@ -1,16 +1,11 @@
/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.listeners;
-
-import java.io.File;
+package edu.kit.kastel.sdq.intelligrade.listeners;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
-import edu.kit.kastel.exceptions.ImplementationMissing;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-import edu.kit.kastel.sdq.artemis4j.grading.config.JsonFileConfig;
-import edu.kit.kastel.utils.AssessmentUtils;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
/**
* This class handles everything related to the grading config and related UI events.
@@ -30,11 +25,6 @@ public void insertUpdate(DocumentEvent documentEvent) {
// store saved grading config path
ArtemisSettingsState settings = ArtemisSettingsState.getInstance();
settings.setSelectedGradingConfigPath(gradingConfigPath);
-
- // parse JSON Data and make it accessible to the listeners
- JsonFileConfig gradingConfig = new JsonFileConfig(new File(gradingConfigPath));
- ExerciseSelectedListener.updateJsonConfig(gradingConfig);
- AssessmentUtils.initExerciseConfig(gradingConfig);
}
@Override
@@ -44,7 +34,7 @@ public void removeUpdate(DocumentEvent documentEvent) {
@Override
public void changedUpdate(DocumentEvent documentEvent) {
- throw new ImplementationMissing(
+ throw new IllegalStateException(
"Wrong event `GradingConfigSelectedListener::changedUpdate` " + "called. This requires bug fixing!");
}
}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnMouseInEditorMoved.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnMouseInEditorMoved.java
new file mode 100644
index 0000000..ead23c1
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnMouseInEditorMoved.java
@@ -0,0 +1,14 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.listeners;
+
+import com.intellij.openapi.editor.event.EditorMouseEvent;
+import com.intellij.openapi.editor.event.EditorMouseMotionListener;
+import edu.kit.kastel.sdq.intelligrade.highlighter.HighlighterManager;
+import org.jetbrains.annotations.NotNull;
+
+public class OnMouseInEditorMoved implements EditorMouseMotionListener {
+ @Override
+ public void mouseMoved(@NotNull EditorMouseEvent e) {
+ HighlighterManager.onMouseMovedInEditor(e);
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/listeners/OnPlugInLoad.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnPlugInLoad.java
similarity index 61%
rename from src/main/java/edu/kit/kastel/listeners/OnPlugInLoad.java
rename to src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnPlugInLoad.java
index 9eeed19..1d153d8 100644
--- a/src/main/java/edu/kit/kastel/listeners/OnPlugInLoad.java
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnPlugInLoad.java
@@ -1,10 +1,11 @@
/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.listeners;
+package edu.kit.kastel.sdq.intelligrade.listeners;
import java.util.List;
import com.intellij.ide.AppLifecycleListener;
-import edu.kit.kastel.utils.ArtemisUtils;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisCredentialsProvider;
+import edu.kit.kastel.sdq.intelligrade.state.PluginState;
import org.jetbrains.annotations.NotNull;
/**
@@ -16,6 +17,7 @@ public class OnPlugInLoad implements AppLifecycleListener {
@Override
public void appFrameCreated(@NotNull List commandLineArgs) {
AppLifecycleListener.super.appFrameCreated(commandLineArgs);
- ArtemisUtils.getArtemisClientInstance();
+ ArtemisCredentialsProvider.getInstance().initialize();
+ PluginState.getInstance().connect();
}
}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnStartupCompleted.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnStartupCompleted.java
new file mode 100644
index 0000000..f11758c
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/listeners/OnStartupCompleted.java
@@ -0,0 +1,31 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.listeners;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.ProjectActivity;
+import com.intellij.openapi.wm.ToolWindowManager;
+import edu.kit.kastel.sdq.intelligrade.highlighter.HighlighterManager;
+import kotlin.Unit;
+import kotlin.coroutines.Continuation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OnStartupCompleted implements ProjectActivity, DumbAware {
+ @Nullable
+ @Override
+ public Object execute(@NotNull Project project, @NotNull Continuation super Unit> continuation) {
+ HighlighterManager.initialize();
+
+ project.getMessageBus().connect().subscribe(DumbService.DUMB_MODE, FileOpener.getInstance());
+
+ // Open the Artemis tool window
+ ApplicationManager.getApplication().invokeLater(() -> ToolWindowManager.getInstance(project)
+ .getToolWindow("Artemis")
+ .show());
+
+ return null;
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefDialog.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefDialog.java
new file mode 100644
index 0000000..cacb05b
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefDialog.java
@@ -0,0 +1,39 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.login;
+
+import java.awt.GridLayout;
+
+import javax.swing.Action;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.ui.jcef.JBCefBrowser;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class CefDialog extends DialogWrapper {
+ private final JBCefBrowser browser;
+
+ public CefDialog(JBCefBrowser browser) {
+ super((Project) null);
+ this.browser = browser;
+
+ this.setTitle("Artemis Login");
+ this.setModal(false);
+ this.init();
+ }
+
+ @Override
+ protected @Nullable JComponent createCenterPanel() {
+ JPanel browserContainer = new JPanel(new GridLayout(1, 1));
+ browserContainer.add(this.browser.getComponent());
+ return browserContainer;
+ }
+
+ @Override
+ protected Action @NotNull [] createActions() {
+ return new Action[0];
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefUtils.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefUtils.java
new file mode 100644
index 0000000..a720bf0
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefUtils.java
@@ -0,0 +1,83 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.login;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+import javax.swing.SwingUtilities;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.ui.jcef.JBCefApp;
+import com.intellij.ui.jcef.JBCefBrowser;
+import com.intellij.ui.jcef.JBCefClient;
+import com.intellij.ui.jcef.JBCefCookie;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import org.cef.CefApp;
+import org.cef.handler.CefFocusHandler;
+
+public final class CefUtils {
+ private static JBCefClient browserClient = JBCefApp.getInstance().createClient();
+
+ static {
+ // TODO choose a different disposer, see
+ // https://plugins.jetbrains.com/docs/intellij/disposers.html?from=IncorrectParentDisposable#choosing-a-disposable-parent
+ Disposer.register(ApplicationManager.getApplication(), browserClient);
+ }
+
+ private CefUtils() {
+ throw new IllegalAccessError("Utility Class");
+ }
+
+ /**
+ * Create and display a Window containing a JBCef Window to request login. Call this on the EDT!
+ *
+ * @return A future on the JWT Cookie to log in. Don't await it on the EDT.
+ */
+ public static CompletableFuture jcefBrowserLogin() {
+
+ if (browserClient == null) {
+ browserClient = JBCefApp.getInstance().createClient();
+ }
+
+ // offscreen rendering is problematic on Linux
+ JBCefBrowser browser = JBCefBrowser.createBuilder()
+ .setClient(browserClient)
+ .setOffScreenRendering(false)
+ .setUrl(ArtemisSettingsState.getInstance().getArtemisInstanceUrl())
+ .build();
+
+ // TODO the following code deletes the jwt cookie, which is needed for a "fresh" login
+ // TODO add this somewhere where it is useful
+ // CefApp.getInstance().onInitialization(state -> {
+ // browser.getJBCefCookieManager().deleteCookies(null, null);
+ // });
+
+ // set focus handler because it gets invoked sometimes and causes NullPE otherwise
+ CefFocusHandler focusHandler = new CefWindowFocusHandler();
+ browserClient.addFocusHandler(focusHandler, browser.getCefBrowser());
+
+ var jwtFuture = new CompletableFuture();
+
+ SwingUtilities.invokeLater(() -> {
+ // create window, display it and navigate to log in URL
+ var window = new CefDialog(browser);
+ window.show();
+
+ JwtRetriever jwtRetriever = new JwtRetriever(browser, window);
+ browserClient.addLoadHandler(jwtRetriever, browser.getCefBrowser());
+
+ // Wait for CEF initialization
+ CefApp.getInstance()
+ .onInitialization(state -> jwtFuture.completeAsync(() -> {
+ try {
+ return jwtRetriever.getJwtCookie();
+ } catch (Exception ex) {
+ throw new CompletionException(ex);
+ }
+ }));
+ });
+
+ return jwtFuture;
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/login/CefWindowFocusHandler.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefWindowFocusHandler.java
similarity index 68%
rename from src/main/java/edu/kit/kastel/login/CefWindowFocusHandler.java
rename to src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefWindowFocusHandler.java
index 43d580e..40f35d2 100644
--- a/src/main/java/edu/kit/kastel/login/CefWindowFocusHandler.java
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/login/CefWindowFocusHandler.java
@@ -1,5 +1,5 @@
/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.login;
+package edu.kit.kastel.sdq.intelligrade.login;
import org.cef.browser.CefBrowser;
import org.cef.handler.CefFocusHandler;
@@ -10,7 +10,9 @@
*/
public class CefWindowFocusHandler implements CefFocusHandler {
@Override
- public void onTakeFocus(CefBrowser cefBrowser, boolean b) {}
+ public void onTakeFocus(CefBrowser cefBrowser, boolean b) {
+ // Nothing to do
+ }
@Override
public boolean onSetFocus(CefBrowser cefBrowser, FocusSource focusSource) {
@@ -18,5 +20,7 @@ public boolean onSetFocus(CefBrowser cefBrowser, FocusSource focusSource) {
}
@Override
- public void onGotFocus(CefBrowser cefBrowser) {}
+ public void onGotFocus(CefBrowser cefBrowser) {
+ // Nothing to do
+ }
}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/login/JwtRetriever.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/login/JwtRetriever.java
new file mode 100644
index 0000000..fe25953
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/login/JwtRetriever.java
@@ -0,0 +1,83 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.login;
+
+import javax.swing.SwingUtilities;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.ui.jcef.JBCefBrowser;
+import com.intellij.ui.jcef.JBCefCookie;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisCredentialsProvider;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import org.cef.browser.CefBrowser;
+import org.cef.browser.CefFrame;
+import org.cef.handler.CefLoadHandlerAdapter;
+import org.cef.network.CefCookieManager;
+
+public class JwtRetriever extends CefLoadHandlerAdapter {
+ private static final Logger LOG = Logger.getInstance(JwtRetriever.class);
+
+ private static final String JWT_COOKIE_KEY = "jwt";
+
+ private final JBCefBrowser browser;
+ private final CefDialog window;
+
+ private volatile JBCefCookie jwtCookie;
+
+ public JwtRetriever(JBCefBrowser browser, CefDialog window) {
+ this.browser = browser;
+ this.window = window;
+ }
+
+ @Override
+ public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
+ synchronized (this) {
+ this.notifyAll();
+ }
+ }
+
+ public JBCefCookie getJwtCookie() throws Exception {
+ var settings = ArtemisSettingsState.getInstance();
+ var credentials = ArtemisCredentialsProvider.getInstance();
+
+ String url = settings.getArtemisInstanceUrl();
+ String jwt = credentials.getJwt();
+
+ synchronized (this) {
+ while (true) {
+ // We may have been woken up because the cookie is available
+ if (jwtCookie != null) {
+ // Can't use ApplicationManager.getApplication().invokeLater here,
+ // as this is only called on application exit on Linux in this specific case (not sure why)
+ SwingUtilities.invokeLater(() -> {
+ window.performOKAction();
+ // window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING));
+ this.browser.getCefBrowser().close(true);
+ });
+ return jwtCookie;
+ }
+
+ // Otherwise, visit all cookies and look for the JWT cookie
+ try {
+ CefCookieManager.getGlobalManager().visitUrlCookies(url, true, (cookie, count, total, delete) -> {
+ if (cookie.name.equals(JWT_COOKIE_KEY)) {
+ if (!cookie.value.equals(jwt)) {
+ synchronized (JwtRetriever.this) {
+ jwtCookie = new JBCefCookie(cookie);
+ this.notifyAll();
+ }
+ }
+ return false;
+ }
+ return true;
+ });
+ } catch (RuntimeException e) {
+ // This can happen if the cookie manager is not yet initialized
+ // In this case, we just wait and try again
+ LOG.warn(e);
+ }
+
+ this.wait(500);
+ }
+ }
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/state/ActiveAssessment.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/state/ActiveAssessment.java
new file mode 100644
index 0000000..777e787
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/state/ActiveAssessment.java
@@ -0,0 +1,235 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.state;
+
+import java.awt.EventQueue;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.ScrollPaneFactory;
+import com.intellij.ui.components.JBPanel;
+import com.intellij.ui.components.JBTextArea;
+import com.intellij.util.ui.JBFont;
+import edu.kit.kastel.sdq.artemis4j.grading.Annotation;
+import edu.kit.kastel.sdq.artemis4j.grading.Assessment;
+import edu.kit.kastel.sdq.artemis4j.grading.ClonedProgrammingSubmission;
+import edu.kit.kastel.sdq.artemis4j.grading.penalty.GradingConfig;
+import edu.kit.kastel.sdq.artemis4j.grading.penalty.MistakeType;
+import edu.kit.kastel.sdq.intelligrade.autograder.AutograderTask;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.AutograderOption;
+import edu.kit.kastel.sdq.intelligrade.utils.ArtemisUtils;
+import edu.kit.kastel.sdq.intelligrade.utils.CodeSelection;
+import edu.kit.kastel.sdq.intelligrade.utils.IntellijUtil;
+import net.miginfocom.swing.MigLayout;
+
+public class ActiveAssessment {
+ private static final Logger LOG = Logger.getInstance(ActiveAssessment.class);
+
+ public static final Path ASSIGNMENT_SUB_PATH = Path.of("assignment");
+
+ private final List>> annotationsUpdatedListener = new ArrayList<>();
+
+ private final Assessment assessment;
+ private final ClonedProgrammingSubmission clonedSubmission;
+
+ public ActiveAssessment(Assessment assessment, ClonedProgrammingSubmission clonedSubmission) {
+ this.assessment = assessment;
+ this.clonedSubmission = clonedSubmission;
+ }
+
+ public void registerAnnotationsUpdatedListener(Consumer> listener) {
+ annotationsUpdatedListener.add(listener);
+ listener.accept(assessment.getAnnotations());
+ }
+
+ public GradingConfig getGradingConfig() {
+ return assessment.getConfig();
+ }
+
+ public void addAnnotationAtCaret(MistakeType mistakeType, boolean withCustomMessage) {
+ if (assessment == null) {
+ throw new IllegalStateException("No active assessment");
+ }
+
+ var selection = CodeSelection.fromCaret();
+ if (selection.isEmpty()) {
+ ArtemisUtils.displayGenericErrorBalloon(
+ "No code selected", "Cannot create annotation without code selection");
+ return;
+ }
+
+ var editor = IntellijUtil.getActiveEditor();
+ int startLine = editor.getDocument().getLineNumber(selection.get().startOffset());
+ int endLine = editor.getDocument().getLineNumber(selection.get().endOffset());
+ String path = Path.of(IntellijUtil.getActiveProject().getBasePath())
+ .resolve(ASSIGNMENT_SUB_PATH)
+ .relativize(selection.get().path())
+ .toString();
+
+ if (mistakeType.isCustomAnnotation()) {
+ addCustomAnnotation(mistakeType, startLine, endLine, path);
+ } else {
+ if (withCustomMessage) {
+ addPredefinedAnnotationWithCustomMessage(mistakeType, startLine, endLine, path);
+ } else {
+ assessment.addPredefinedAnnotation(mistakeType, path, startLine, endLine, null);
+ this.notifyListeners();
+ }
+ }
+ }
+
+ public void deleteAnnotation(Annotation annotation) {
+ this.assessment.removeAnnotation(annotation);
+ this.notifyListeners();
+ }
+
+ public void runAutograder() {
+ var settings = ArtemisSettingsState.getInstance();
+ if (settings.getAutograderOption() == AutograderOption.SKIP) {
+ return;
+ }
+
+ AutograderTask.execute(assessment, clonedSubmission, this::notifyListeners);
+ }
+
+ public Assessment getAssessment() {
+ return this.assessment;
+ }
+
+ public void changeCustomMessage(Annotation annotation) {
+ if (annotation.getMistakeType().isCustomAnnotation()) {
+ showCustomAnnotationDialog(
+ annotation.getMistakeType(),
+ annotation.getCustomMessage().orElseThrow(),
+ annotation.getCustomScore().orElseThrow(),
+ messageWithPoints -> {
+ annotation.setCustomMessage(messageWithPoints.message());
+ annotation.setCustomScore(messageWithPoints.points());
+ this.notifyListeners();
+ });
+ } else {
+ showCustomMessageDialog(annotation.getCustomMessage().orElse(""), customMessage -> {
+ if (customMessage.isBlank()) {
+ annotation.setCustomMessage(null);
+ } else {
+ annotation.setCustomMessage(customMessage);
+ }
+ this.notifyListeners();
+ });
+ }
+ }
+
+ private void addPredefinedAnnotationWithCustomMessage(
+ MistakeType mistakeType, int startLine, int endLine, String path) {
+ showCustomMessageDialog("", customMessage -> {
+ this.assessment.addPredefinedAnnotation(mistakeType, path, startLine, endLine, customMessage);
+ this.notifyListeners();
+ });
+ }
+
+ private void addCustomAnnotation(MistakeType mistakeType, int startLine, int endLine, String path) {
+ showCustomAnnotationDialog(mistakeType, "", 0.0, messageWithPoints -> {
+ this.assessment.addCustomAnnotation(
+ mistakeType, path, startLine, endLine, messageWithPoints.message(), messageWithPoints.points());
+ this.notifyListeners();
+ });
+ }
+
+ private void notifyListeners() {
+ for (Consumer> listener : this.annotationsUpdatedListener) {
+ listener.accept(this.assessment.getAnnotations());
+ }
+ }
+
+ private void showCustomMessageDialog(String initialMessage, Consumer onOk) {
+ var panel = new JBPanel<>(new MigLayout("wrap 1", "[250lp]"));
+
+ var customMessage = new JBTextArea(initialMessage);
+ customMessage.setFont(JBFont.regular());
+ customMessage.setBorder(BorderFactory.createLineBorder(JBColor.border()));
+ panel.add(ScrollPaneFactory.createScrollPane(customMessage), "grow, height 100lp");
+
+ var popup = JBPopupFactory.getInstance()
+ .createComponentPopupBuilder(panel, customMessage)
+ .setTitle("Custom Message")
+ .setFocusable(true)
+ .setRequestFocus(true)
+ .setMovable(true)
+ .setResizable(true)
+ .setNormalWindowLevel(true)
+ .setOkHandler(() -> onOk.accept(customMessage.getText().trim()))
+ .createPopup();
+
+ customMessage.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+ if (e.isControlDown()) {
+ customMessage.insert("\n", customMessage.getCaretPosition());
+ } else {
+ popup.closeOk((InputEvent) EventQueue.getCurrentEvent());
+ }
+ }
+ }
+ });
+
+ popup.showCenteredInCurrentWindow(IntellijUtil.getActiveProject());
+ }
+
+ private void showCustomAnnotationDialog(
+ MistakeType mistakeType, String initialMessage, double initialPoints, Consumer onOk) {
+ var panel = new JBPanel<>(new MigLayout("wrap 2", "[200lp] []"));
+
+ var customMessage = new JBTextArea(initialMessage);
+ customMessage.setFont(JBFont.regular());
+ customMessage.setBorder(BorderFactory.createLineBorder(JBColor.border()));
+ panel.add(ScrollPaneFactory.createScrollPane(customMessage), "span 2, grow, height 100lp");
+
+ double maxValue = this.assessment.getConfig().isPositiveFeedbackAllowed() ? Double.MAX_VALUE : 0.0;
+ double minValue = mistakeType.getRatingGroup().getMinPenalty();
+ var customScore = new JSpinner(new SpinnerNumberModel(initialPoints, minValue, maxValue, 0.5));
+ panel.add(customScore, "spanx 2, growx");
+
+ var okButton = new JButton("Ok");
+ panel.add(okButton, "skip 1, tag ok");
+
+ var popup = JBPopupFactory.getInstance()
+ .createComponentPopupBuilder(panel, customMessage)
+ .setTitle("Custom Comment")
+ .setFocusable(true)
+ .setRequestFocus(true)
+ .setMovable(true)
+ .setResizable(true)
+ .setNormalWindowLevel(true)
+ .setOkHandler(() -> onOk.accept(
+ new MessageWithPoints(customMessage.getText().trim(), (double) customScore.getValue())))
+ .createPopup();
+
+ okButton.addActionListener(a -> popup.closeOk((InputEvent) EventQueue.getCurrentEvent()));
+ customMessage.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_ENTER && !e.isControlDown()) {
+ popup.closeOk((InputEvent) EventQueue.getCurrentEvent());
+ }
+ }
+ });
+
+ popup.showCenteredInCurrentWindow(IntellijUtil.getActiveProject());
+ }
+
+ private record MessageWithPoints(String message, double points) {}
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/state/PluginState.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/state/PluginState.java
new file mode 100644
index 0000000..aad1fac
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/state/PluginState.java
@@ -0,0 +1,514 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.state;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Task;
+import com.intellij.ui.jcef.JBCefApp;
+import edu.kit.kastel.sdq.artemis4j.ArtemisClientException;
+import edu.kit.kastel.sdq.artemis4j.ArtemisNetworkException;
+import edu.kit.kastel.sdq.artemis4j.client.ArtemisInstance;
+import edu.kit.kastel.sdq.artemis4j.grading.ArtemisConnection;
+import edu.kit.kastel.sdq.artemis4j.grading.Assessment;
+import edu.kit.kastel.sdq.artemis4j.grading.MoreRecentSubmissionException;
+import edu.kit.kastel.sdq.artemis4j.grading.ProgrammingExercise;
+import edu.kit.kastel.sdq.artemis4j.grading.ProgrammingSubmission;
+import edu.kit.kastel.sdq.artemis4j.grading.metajson.AnnotationMappingException;
+import edu.kit.kastel.sdq.artemis4j.grading.penalty.GradingConfig;
+import edu.kit.kastel.sdq.artemis4j.grading.penalty.InvalidGradingConfigException;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisCredentialsProvider;
+import edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettingsState;
+import edu.kit.kastel.sdq.intelligrade.login.CefUtils;
+import edu.kit.kastel.sdq.intelligrade.utils.ArtemisUtils;
+import edu.kit.kastel.sdq.intelligrade.utils.IntellijUtil;
+import org.apache.commons.io.FileUtils;
+import org.jetbrains.annotations.NotNull;
+
+public class PluginState {
+ private static final Logger LOG = Logger.getInstance(PluginState.class);
+
+ private static PluginState pluginState;
+
+ private final List>> connectedListeners = new ArrayList<>();
+ private final List> assessmentStartedListeners = new ArrayList<>();
+ private final List assessmentClosedListeners = new ArrayList<>();
+
+ private ArtemisConnection connection;
+ private ProgrammingExercise activeExercise;
+
+ private ActiveAssessment activeAssessment;
+
+ public static PluginState getInstance() {
+ if (pluginState == null) {
+ pluginState = new PluginState();
+ }
+ return pluginState;
+ }
+
+ public void connect() {
+ this.resetState();
+
+ var settings = ArtemisSettingsState.getInstance();
+ var credentials = ArtemisCredentialsProvider.getInstance();
+
+ String url = settings.getArtemisInstanceUrl();
+ if (!ArtemisUtils.doesUrlExist(url)) {
+ ArtemisUtils.displayGenericErrorBalloon(
+ "Artemis URL not reachable",
+ "The Artemis URL is not valid, or you do not have a working internet connection.");
+ this.notifyConnectedListeners();
+ return;
+ }
+
+ var instance = new ArtemisInstance(settings.getArtemisInstanceUrl());
+
+ CompletableFuture connectionFuture;
+ if (settings.isUseTokenLogin()) {
+ connectionFuture = retrieveJWT().thenApplyAsync(token -> ArtemisConnection.fromToken(instance, token));
+ } else {
+ connectionFuture = CompletableFuture.supplyAsync(() -> {
+ try {
+ return ArtemisConnection.connectWithUsernamePassword(
+ instance, settings.getUsername(), credentials.getArtemisPassword());
+ } catch (ArtemisClientException e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ connectionFuture
+ .thenAcceptAsync(newConnection -> {
+ this.connection = newConnection;
+ try {
+ this.verifyLogin();
+ this.notifyConnectedListeners();
+ } catch (ArtemisClientException e) {
+ throw new CompletionException(e);
+ }
+ })
+ .exceptionallyAsync(e -> {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon("Artemis Login failed", e.getMessage());
+ this.connection = null;
+ this.notifyConnectedListeners();
+ return null;
+ });
+ }
+
+ public void registerConnectedListener(Consumer> listener) {
+ this.connectedListeners.add(listener);
+ listener.accept(Optional.ofNullable(this.connection));
+ }
+
+ public boolean isAssessing() {
+ return activeAssessment != null;
+ }
+
+ public void startNextAssessment(int correctionRound) {
+ if (activeAssessment != null) {
+ ArtemisUtils.displayFinishAssessmentFirstBalloon();
+ return;
+ }
+
+ if (activeExercise == null) {
+ ArtemisUtils.displayGenericErrorBalloon("Could not start assessment", "No course selected");
+ return;
+ }
+
+ var gradingConfig = createGradingConfig();
+ if (gradingConfig.isEmpty()) {
+ return;
+ }
+
+ new StartAssessmentTask(correctionRound, gradingConfig.get()).queue();
+ }
+
+ public void saveAssessment() {
+ if (activeAssessment == null) {
+ ArtemisUtils.displayNoAssessmentBalloon();
+ return;
+ }
+
+ try {
+ activeAssessment.getAssessment().save();
+ ArtemisUtils.displayGenericInfoBalloon("Assessment saved", "The assessment has been saved.");
+ } catch (ArtemisNetworkException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayNetworkErrorBalloon("Could not save assessment", e);
+ } catch (AnnotationMappingException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon(
+ "Could not save assessment",
+ "Failed to serialize the assessment. This is a serious bug; please contact the Übungsleitung!");
+ }
+ }
+
+ public void submitAssessment() {
+ if (activeAssessment == null) {
+ ArtemisUtils.displayNoAssessmentBalloon();
+ return;
+ }
+
+ try {
+ activeAssessment.getAssessment().submit();
+ this.cleanupAssessment();
+ } catch (ArtemisNetworkException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayNetworkErrorBalloon("Could not submit assessment", e);
+ } catch (AnnotationMappingException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon(
+ "Could not submit assessment",
+ "Failed to serialize the assessment. This is a serious bug; please contact the Übungsleitung!");
+ }
+ }
+
+ public void cancelAssessment() {
+ if (activeAssessment == null) {
+ ArtemisUtils.displayNoAssessmentBalloon();
+ return;
+ }
+
+ try {
+ activeAssessment.getAssessment().cancel();
+ this.cleanupAssessment();
+ } catch (ArtemisNetworkException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayNetworkErrorBalloon("Could not submit assessment", e);
+ }
+ }
+
+ public void closeAssessment() {
+ if (activeAssessment == null) {
+ ArtemisUtils.displayNoAssessmentBalloon();
+ return;
+ }
+
+ this.cleanupAssessment();
+ }
+
+ public void reopenAssessment(ProgrammingSubmission submission) {
+ if (activeAssessment != null) {
+ ArtemisUtils.displayFinishAssessmentFirstBalloon();
+ return;
+ }
+
+ if (activeExercise == null) {
+ ArtemisUtils.displayGenericErrorBalloon("Could not reopen assessment", "No exercise selected");
+ return;
+ }
+
+ var gradingConfig = createGradingConfig();
+ if (gradingConfig.isEmpty()) {
+ return;
+ }
+
+ new ReopenAssessmentTask(submission, gradingConfig.get()).queue();
+ }
+
+ public Optional getActiveExercise() {
+ return Optional.ofNullable(activeExercise);
+ }
+
+ public void setActiveExercise(ProgrammingExercise exercise) {
+ this.activeExercise = exercise;
+ }
+
+ public Optional getActiveAssessment() {
+ return Optional.ofNullable(activeAssessment);
+ }
+
+ public void registerAssessmentStartedListener(Consumer listener) {
+ this.assessmentStartedListeners.add(listener);
+ if (this.isAssessing()) {
+ listener.accept(activeAssessment);
+ }
+ }
+
+ public void registerAssessmentClosedListener(Runnable listener) {
+ this.assessmentClosedListeners.add(listener);
+ }
+
+ private void resetState() {
+ connection = null;
+ activeExercise = null;
+ activeAssessment = null;
+ }
+
+ private CompletableFuture retrieveJWT() {
+ var settings = ArtemisSettingsState.getInstance();
+ var credentials = ArtemisCredentialsProvider.getInstance();
+
+ return CompletableFuture.supplyAsync(() -> {
+ String previousJwt = credentials.getJwt();
+ if (previousJwt != null && !previousJwt.isBlank()) {
+ return previousJwt;
+ }
+
+ if (!JBCefApp.isSupported()) {
+ throw new CompletionException(new IllegalStateException("JCEF unavailable"));
+ }
+
+ try {
+ var cookie = CefUtils.jcefBrowserLogin().get();
+ credentials.setJwt(cookie.getValue());
+ settings.setJwtExpiry(cookie.getExpires());
+ return cookie.getValue();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new CompletionException(ex);
+ } catch (ExecutionException e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ private void verifyLogin() throws ArtemisClientException {
+ // This triggers a request and forces a connection error if the token is invalid
+ this.connection.getAssessor();
+ }
+
+ private void notifyConnectedListeners() {
+ ApplicationManager.getApplication().invokeLater(() -> {
+ for (Consumer> l : this.connectedListeners) {
+ l.accept(Optional.ofNullable(this.connection));
+ }
+ });
+ }
+
+ private boolean initializeAssessment(Assessment assessment) {
+ try {
+ // Cleanup first, in case there are files left from a previous assessment
+ this.cleanupProjectDirectory();
+
+ // Clone the new submission
+ var clonedSubmission =
+ switch (ArtemisSettingsState.getInstance().getVcsAccessOption()) {
+ case SSH -> {
+ // We need to switch the classloader here (same as
+ // https://plugins.jetbrains.com/docs/intellij/plugin-class-loaders.html#using-serviceloader)
+ // Somewhere deep in the auth libs, an instanceof check is performed, which returns false in
+ // some cases where the same class was loaded with two different class loaders (the plugin
+ // and the platform ones)
+ Thread currentThread = Thread.currentThread();
+ ClassLoader originalClassLoader = currentThread.getContextClassLoader();
+ ClassLoader pluginClassLoader = this.getClass().getClassLoader();
+ try {
+ currentThread.setContextClassLoader(pluginClassLoader);
+ yield assessment
+ .getSubmission()
+ .cloneViaSSHInto(IntellijUtil.getProjectRootDirectory());
+ } finally {
+ currentThread.setContextClassLoader(originalClassLoader);
+ }
+ }
+ case TOKEN -> assessment
+ .getSubmission()
+ .cloneViaVCSTokenInto(IntellijUtil.getProjectRootDirectory(), null);
+ };
+
+ // Refresh all files, so that they are up-to-date for the maven update
+ IntellijUtil.forceFilesSync(() -> {
+ // Force IntelliJ to update the Maven project
+ IntellijUtil.getMavenManager().forceUpdateAllProjectsOrFindAllAvailablePomFiles();
+ });
+
+ this.activeAssessment = new ActiveAssessment(assessment, clonedSubmission);
+ for (Consumer listener : this.assessmentStartedListeners) {
+ listener.accept(activeAssessment);
+ }
+
+ return true;
+ } catch (ArtemisClientException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon("Error cloning submission", e.getMessage());
+
+ // Cancel the assessment to prevent spurious locks
+ try {
+ assessment.cancel();
+ } catch (ArtemisNetworkException ex) {
+ LOG.warn(ex);
+ ArtemisUtils.displayGenericErrorBalloon("Failed to free the assessment lock", ex.getMessage());
+ }
+
+ return false;
+ }
+ }
+
+ private void cleanupAssessment() {
+ this.activeAssessment = null;
+
+ // Do not close the ClonedProgrammingSubmission, since this would try to delete the workspace file
+ // Instead, we delete the project directory manually
+ this.cleanupProjectDirectory();
+
+ // Tell IntelliJ's VCS manager that the Git repo is gone
+ // This prevents an annoying popup that warns about a missing Git root
+ IntellijUtil.forceFilesSync(() -> {
+ IntellijUtil.getVcsManager().setDirectoryMappings(List.of());
+ IntellijUtil.getVcsManager().fireDirectoryMappingsChanged();
+ });
+
+ for (Runnable assessmentClosedListener : this.assessmentClosedListeners) {
+ assessmentClosedListener.run();
+ }
+ }
+
+ private Optional createGradingConfig() {
+ var gradingConfigPath = ArtemisSettingsState.getInstance().getSelectedGradingConfigPath();
+ if (gradingConfigPath == null) {
+ ArtemisUtils.displayGenericErrorBalloon("No grading config", "Please select a grading config");
+ return Optional.empty();
+ }
+
+ try {
+ return Optional.of(
+ GradingConfig.readFromString(Files.readString(Path.of(gradingConfigPath)), activeExercise));
+ } catch (IOException | InvalidGradingConfigException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon("Invalid grading config", e.getMessage());
+ return Optional.empty();
+ }
+ }
+
+ private void cleanupProjectDirectory() {
+ // Close all open editors
+ var editorManager = FileEditorManager.getInstance(IntellijUtil.getActiveProject());
+ ApplicationManager.getApplication().invokeAndWait(() -> {
+ for (var editor : editorManager.getAllEditors()) {
+ editorManager.closeFile(editor.getFile());
+ }
+ });
+
+ var rootPath = IntellijUtil.getProjectRootDirectory();
+ // Delete all directory contents, but not the directory itself
+ try {
+ Files.walkFileTree(rootPath, new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ FileUtils.forceDelete(file.toFile());
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ if (!dir.equals(rootPath)) {
+ FileUtils.forceDelete(dir.toFile());
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (IOException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon("Error cleaning up project directory", e.getMessage());
+ }
+ }
+
+ private class StartAssessmentTask extends Task.Modal {
+ private final int correctionRound;
+ private final GradingConfig gradingConfig;
+
+ public StartAssessmentTask(int correctionRound, GradingConfig gradingConfig) {
+ super(IntellijUtil.getActiveProject(), "Starting Assessment", false);
+ this.correctionRound = correctionRound;
+ this.gradingConfig = gradingConfig;
+ }
+
+ @Override
+ public void run(@NotNull ProgressIndicator progressIndicator) {
+ try {
+ progressIndicator.setText("Locking...");
+ var nextAssessment = activeExercise.tryLockNextSubmission(correctionRound, gradingConfig);
+ if (nextAssessment.isPresent()) {
+ progressIndicator.setText("Cloning...");
+ if (!initializeAssessment(nextAssessment.get())) {
+ return;
+ }
+
+ // Now everything is done - the submission is properly locked, and the repository is cloned
+ if (activeAssessment.getAssessment().getAnnotations().isEmpty()) {
+ activeAssessment.runAutograder();
+ } else {
+ ArtemisUtils.displayGenericInfoBalloon(
+ "Skipping Autograder",
+ "The submission already has annotations. Skipping the Autograder.");
+ }
+
+ ArtemisUtils.displayGenericInfoBalloon(
+ "Assessment started",
+ "You can now grade the submission. Please make sure that are familiar with all "
+ + "grading guidelines.");
+ } else {
+ ArtemisUtils.displayGenericInfoBalloon(
+ "Could not start assessment",
+ "There are no more submissions to assess. Thanks for your work :)");
+ }
+ } catch (ArtemisNetworkException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayNetworkErrorBalloon("Could not lock assessment", e);
+ } catch (AnnotationMappingException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon(
+ "Could not parse assessment",
+ "Could not parse previous assessment. This is a serious bug; please contact the "
+ + "Übungsleitung!");
+ }
+ }
+ }
+
+ private class ReopenAssessmentTask extends Task.Modal {
+ private final ProgrammingSubmission submission;
+ private final GradingConfig gradingConfig;
+
+ public ReopenAssessmentTask(ProgrammingSubmission submission, GradingConfig gradingConfig) {
+ super(IntellijUtil.getActiveProject(), "Reopening Assessment", false);
+ this.submission = submission;
+ this.gradingConfig = gradingConfig;
+ }
+
+ @Override
+ public void run(@NotNull ProgressIndicator progressIndicator) {
+ try {
+ progressIndicator.setText("Locking...");
+ var assessment = submission.tryLock(gradingConfig);
+ if (assessment.isPresent()) {
+ progressIndicator.setText("Cloning...");
+ initializeAssessment(assessment.get());
+ } else {
+ ArtemisUtils.displayGenericErrorBalloon(
+ "Failed to reopen assessment", "Most likely, your lock has been taken by someone else.");
+ }
+
+ } catch (ArtemisNetworkException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayNetworkErrorBalloon("Could not lock assessment", e);
+ } catch (AnnotationMappingException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon(
+ "Could not parse assessment",
+ "Could not parse previous assessment. This is a serious bug; please contact the "
+ + "Übungsleitung!");
+ } catch (MoreRecentSubmissionException e) {
+ LOG.warn(e);
+ ArtemisUtils.displayGenericErrorBalloon(
+ "Could not reopen assessment", "The student has submitted a newer version of his code.");
+ }
+ }
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/ArtemisUtils.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/ArtemisUtils.java
new file mode 100644
index 0000000..7c20e20
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/ArtemisUtils.java
@@ -0,0 +1,64 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.utils;
+
+import java.net.HttpURLConnection;
+import java.net.URI;
+
+import com.intellij.notification.NotificationGroupManager;
+import com.intellij.notification.NotificationType;
+import edu.kit.kastel.sdq.artemis4j.ArtemisNetworkException;
+
+/**
+ * Utility Class to handle Artemis related common tasks such as
+ * creating a new client and logging in or creating Error messages.
+ */
+public final class ArtemisUtils {
+ private ArtemisUtils() {}
+
+ public static boolean doesUrlExist(String url) {
+ try {
+ var connection = (HttpURLConnection) new URI(url).toURL().openConnection();
+ return connection.getResponseCode() == HttpURLConnection.HTTP_OK;
+ } catch (Exception ex) {
+ return false;
+ }
+ }
+
+ public static void displayGenericErrorBalloon(String title, String content) {
+ NotificationGroupManager.getInstance()
+ .getNotificationGroup("IntelliGrade Notifications")
+ .createNotification(content, NotificationType.ERROR)
+ .setTitle(title)
+ .notify(null);
+ }
+
+ public static void displayGenericWarningBalloon(String title, String content) {
+ NotificationGroupManager.getInstance()
+ .getNotificationGroup("IntelliGrade Notifications")
+ .createNotification(content, NotificationType.WARNING)
+ .setTitle(title)
+ .notify(null);
+ }
+
+ public static void displayGenericInfoBalloon(String title, String content) {
+ NotificationGroupManager.getInstance()
+ .getNotificationGroup("IntelliGrade Notifications")
+ .createNotification(content, NotificationType.INFORMATION)
+ .setTitle(title)
+ .notify(null);
+ }
+
+ public static void displayNetworkErrorBalloon(String content, ArtemisNetworkException cause) {
+ displayGenericErrorBalloon("Network Error", content + " (" + cause.getMessage() + ")");
+ }
+
+ public static void displayNoAssessmentBalloon() {
+ displayGenericWarningBalloon("No active assessment", "Please start an assessment first.");
+ }
+
+ public static void displayFinishAssessmentFirstBalloon() {
+ displayGenericWarningBalloon(
+ "Finish assessment first",
+ "Please finish the current assessment first. If you do not want to, please cancel it.");
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/CodeSelection.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/CodeSelection.java
new file mode 100644
index 0000000..5919cee
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/CodeSelection.java
@@ -0,0 +1,31 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.utils;
+
+import java.nio.file.Path;
+import java.util.Optional;
+
+public record CodeSelection(int startOffset, int endOffset, Path path) {
+
+ public static Optional fromCaret() {
+ var editor = IntellijUtil.getActiveEditor();
+ if (editor == null) {
+ // no editor open or no selection made
+ return Optional.empty();
+ }
+
+ var caret = editor.getCaretModel().getPrimaryCaret();
+
+ int startOffset;
+ int endOffset;
+ if (caret.hasSelection()) {
+ startOffset = caret.getSelectionRange().getStartOffset();
+ endOffset = caret.getSelectionRange().getEndOffset();
+ } else {
+ startOffset = caret.getOffset();
+ endOffset = caret.getOffset();
+ }
+
+ var path = editor.getVirtualFile().toNioPath();
+ return Optional.of(new CodeSelection(startOffset, endOffset, path));
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/IntellijUtil.java b/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/IntellijUtil.java
new file mode 100644
index 0000000..aaa1115
--- /dev/null
+++ b/src/main/java/edu/kit/kastel/sdq/intelligrade/utils/IntellijUtil.java
@@ -0,0 +1,66 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel.sdq.intelligrade.utils;
+
+import java.nio.file.Path;
+import java.util.Objects;
+
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.newvfs.RefreshQueue;
+import edu.kit.kastel.sdq.artemis4j.grading.Annotation;
+import edu.kit.kastel.sdq.intelligrade.state.ActiveAssessment;
+import org.jetbrains.idea.maven.project.MavenProjectsManager;
+
+public final class IntellijUtil {
+ private IntellijUtil() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ public static Project getActiveProject() {
+ return ProjectManager.getInstance().getOpenProjects()[0];
+ }
+
+ public static Editor getActiveEditor() {
+ return FileEditorManager.getInstance(getActiveProject()).getSelectedTextEditor();
+ }
+
+ public static Path getProjectRootDirectory() {
+ return Path.of(getActiveProject().getBasePath());
+ }
+
+ public static MavenProjectsManager getMavenManager() {
+ return MavenProjectsManager.getInstance(getActiveProject());
+ }
+
+ public static void forceFilesSync(Runnable afterSyncAction) {
+ var rootVirtualFile = Objects.requireNonNull(
+ VfsUtil.findFileByIoFile(getProjectRootDirectory().toFile(), true));
+ var session = RefreshQueue.getInstance().createSession(true, true, afterSyncAction);
+ session.addFile(rootVirtualFile);
+ session.launch();
+ }
+
+ public static ProjectLevelVcsManagerImpl getVcsManager() {
+ return ProjectLevelVcsManagerImpl.getInstanceImpl(IntellijUtil.getActiveProject());
+ }
+
+ public static Path getAnnotationPath(Annotation annotation) {
+ return IntellijUtil.getProjectRootDirectory()
+ .resolve(ActiveAssessment.ASSIGNMENT_SUB_PATH)
+ .resolve(annotation.getFilePath());
+ }
+
+ public static VirtualFile getAnnotationFile(Annotation annotation) {
+ var path = getAnnotationPath(annotation);
+ var file = VfsUtil.findFile(path, true);
+ if (file == null) {
+ throw new IllegalStateException("File not found: " + path);
+ }
+ return file;
+ }
+}
diff --git a/src/main/java/edu/kit/kastel/state/AssessmentModeHandler.java b/src/main/java/edu/kit/kastel/state/AssessmentModeHandler.java
deleted file mode 100644
index bd22836..0000000
--- a/src/main/java/edu/kit/kastel/state/AssessmentModeHandler.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.state;
-
-import java.awt.Color;
-import java.util.Optional;
-
-import com.intellij.ui.JBColor;
-import com.intellij.ui.components.JBLabel;
-import edu.kit.kastel.sdq.artemis4j.grading.config.ExerciseConfig;
-import edu.kit.kastel.utils.AssessmentUtils;
-import edu.kit.kastel.wrappers.ExtendedLockResult;
-
-/**
- * Class to handle assessment mode state. This class is a Singleton.
- */
-public class AssessmentModeHandler {
-
- private static AssessmentModeHandler assessmentModeHandler;
- private static final String ASSESSMENT_MODE_ENABLED = "✅";
- private static final String ASSESSMENT_MODE_DISABLED = "❌";
-
- private boolean assessmentMode = false;
- private Optional currentExerciseConfig = Optional.empty();
- private Optional assessmentLock;
-
- private Optional indicatorLabel = Optional.empty();
-
- private AssessmentModeHandler() {}
-
- public static AssessmentModeHandler getInstance() {
- if (assessmentModeHandler == null) {
- assessmentModeHandler = new AssessmentModeHandler();
- }
- return assessmentModeHandler;
- }
-
- public void enableAssessmentMode(ExtendedLockResult assLock) {
- this.assessmentLock = Optional.of(assLock);
- this.assessmentMode = true;
- AssessmentUtils.resetAnnotations();
- this.indicatorLabel.ifPresent(jbLabel -> {
- jbLabel.setText(ASSESSMENT_MODE_ENABLED);
- jbLabel.setBackground(new JBColor(new Color(54, 155, 54), new Color(54, 155, 54)));
- });
- }
-
- public void disableAssessmentMode() {
- this.assessmentMode = false;
- this.assessmentLock = Optional.empty();
- this.indicatorLabel.ifPresent(jbLabel -> {
- jbLabel.setText(ASSESSMENT_MODE_DISABLED);
- jbLabel.setBackground(new JBColor(new Color(155, 54, 54), new Color(155, 54, 54)));
- });
- }
-
- public boolean isInAssesmentMode() {
- return this.assessmentMode;
- }
-
- public Optional getAssessmentLock() {
- return assessmentLock;
- }
-
- public void connectIndicatorLabel(JBLabel label) {
- this.indicatorLabel = Optional.of(label);
- }
-
- public Optional getCurrentExerciseConfig() {
- return currentExerciseConfig;
- }
-
- public void setCurrentExerciseConfig(ExerciseConfig currentExerciseConfig) {
- this.currentExerciseConfig = Optional.of(currentExerciseConfig);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/utils/AnnotationUtils.java b/src/main/java/edu/kit/kastel/utils/AnnotationUtils.java
deleted file mode 100644
index 46bfbc0..0000000
--- a/src/main/java/edu/kit/kastel/utils/AnnotationUtils.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.utils;
-
-import java.awt.*;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-import com.intellij.DynamicBundle;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.editor.Editor;
-import com.intellij.openapi.editor.markup.EffectType;
-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.openapi.fileEditor.FileEditorManager;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.project.ProjectManager;
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiDocumentManager;
-import com.intellij.psi.PsiElement;
-import com.intellij.ui.JBColor;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-import edu.kit.kastel.listeners.OnSubmitAssessmentBtnClick;
-import edu.kit.kastel.sdq.artemis4j.api.grading.IAnnotation;
-import edu.kit.kastel.sdq.artemis4j.api.grading.IMistakeType;
-import edu.kit.kastel.sdq.artemis4j.grading.model.annotation.AnnotationException;
-import edu.kit.kastel.state.AssessmentModeHandler;
-import edu.kit.kastel.wrappers.AnnotationWithTextSelection;
-import org.apache.commons.io.FilenameUtils;
-
-public final class AnnotationUtils {
-
- private static final String ANNOT_ADD_ERR = "Error adding annotation.";
- private static final String NO_ASSESSMENT_MSG = "Please start an assessment first";
-
- private AnnotationUtils() {
- throw new IllegalAccessError("Utility class");
- }
-
- public static void addAnnotationByMistakeType(IMistakeType mistakeType) {
- if (!AssessmentModeHandler.getInstance().isInAssesmentMode()) {
- ArtemisUtils.displayGenericErrorBalloon(NO_ASSESSMENT_MSG);
- return;
- }
-
- Project currentProject = ProjectManager.getInstance().getOpenProjects()[0];
-
- Editor editor = FileEditorManager.getInstance(currentProject).getSelectedTextEditor();
-
- if (editor == null || !editor.getSelectionModel().hasSelection()) {
- // no editor open or no selection made
- return;
- }
-
- // get editor Selection
- TextRange selectedText = editor.getCaretModel().getPrimaryCaret().getSelectionRange();
-
- // only annotate if a selection has been made
- // get the currently selected element and the containing file
- PsiElement selectedElement = PsiDocumentManager.getInstance(currentProject)
- .getPsiFile(editor.getDocument())
- .findElementAt(editor.getCaretModel().getOffset())
- .getContext();
-
- Path subtracted = Paths.get(selectedElement.getProject().getBasePath())
- .relativize(selectedElement.getContainingFile().getVirtualFile().toNioPath());
-
- // Add highlight in Editor
- Color annotationColor = ArtemisSettingsState.getInstance().getAnnotationColor();
-
- TextAttributes annotationMarkup = new TextAttributes(
- null, new JBColor(annotationColor, annotationColor), null, EffectType.BOLD_LINE_UNDERSCORE, Font.PLAIN);
-
- RangeHighlighter highlighter = editor.getMarkupModel()
- .addRangeHighlighter(
- selectedText.getStartOffset(),
- selectedText.getEndOffset(),
- HighlighterLayer.SELECTION - 1,
- annotationMarkup,
- HighlighterTargetArea.EXACT_RANGE);
-
- // add tooltip (on the right bar)
- highlighter.setErrorStripeMarkColor(JBColor.CYAN);
- highlighter.setThinErrorStripeMark(true);
- highlighter.setErrorStripeTooltip(
- mistakeType.getButtonText(DynamicBundle.getLocale().getLanguage()));
-
- // create and add the annotation
- var annotation = new AnnotationWithTextSelection(
- IAnnotation.createID(),
- mistakeType,
- // lines are 0 indexed
- editor.getCaretModel()
- .getPrimaryCaret()
- .getSelectionStartPosition()
- .getLine()
- + 1,
- editor.getCaretModel()
- .getPrimaryCaret()
- .getSelectionEndPosition()
- .getLine()
- + 1,
- FilenameUtils.removeExtension(subtracted.toString()),
- "",
- 0.0,
- highlighter);
-
- try {
- AssessmentUtils.addAnnotation(annotation);
- } catch (AnnotationException e) {
- ArtemisUtils.displayGenericErrorBalloon(ANNOT_ADD_ERR);
- Logger.getInstance(OnSubmitAssessmentBtnClick.class).error(e);
-
- // if an adding the annotation occurs, we remove the highlighter
- editor.getMarkupModel().removeHighlighter(highlighter);
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/utils/ArtemisUtils.java b/src/main/java/edu/kit/kastel/utils/ArtemisUtils.java
deleted file mode 100644
index 36c4459..0000000
--- a/src/main/java/edu/kit/kastel/utils/ArtemisUtils.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.utils;
-
-import com.intellij.notification.Notification;
-import com.intellij.notification.NotificationAction;
-import com.intellij.notification.NotificationGroupManager;
-import com.intellij.notification.NotificationType;
-import com.intellij.openapi.actionSystem.AnAction;
-import com.intellij.openapi.actionSystem.AnActionEvent;
-import com.intellij.openapi.options.ShowSettingsUtil;
-import edu.kit.kastel.extensions.settings.ArtemisSettingsState;
-import edu.kit.kastel.login.CustomLoginManager;
-import edu.kit.kastel.sdq.artemis4j.api.ArtemisClientException;
-import edu.kit.kastel.sdq.artemis4j.client.RestClientManager;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Utility Class to handle Artemis related common tasks such as
- * creating a new client and logging in or creating Error messages.
- */
-public final class ArtemisUtils {
- private static final String LOGIN_ERROR_DIALOG_TITLE = "IntelliGrade Login error";
- public static final String GENERIC_ARTEMIS_ERROR_TITLE = "Artemis Error";
-
- private static RestClientManager artemisClient;
-
- private ArtemisUtils() {
- throw new IllegalAccessError("Utility Class Constructor");
- }
-
- /**
- * get an instance of the Artemis Client. Create one if necessary (singleton).
- *
- * @return the instance persisted or created
- */
- public static @NotNull RestClientManager getArtemisClientInstance() {
- if (artemisClient == null) {
- // retrieve settings
- ArtemisSettingsState settings = ArtemisSettingsState.getInstance();
-
- var tokenLoginManager = new CustomLoginManager(
- settings.getArtemisInstanceUrl(), settings.getUsername(), settings.getArtemisPassword());
-
- // create new Artemis Instance
- var artemisInstance = new RestClientManager(settings.getArtemisInstanceUrl(), tokenLoginManager);
-
- // try logging in
- try {
- tokenLoginManager.login();
- } catch (ArtemisClientException clientException) {
- ArtemisUtils.displayLoginErrorBalloon(
- String.format(
- "%s. This will make the grading PlugIn unusable!%n", clientException.getMessage()),
- new NotificationAction("Configure...") {
- @Override
- public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) {
- ShowSettingsUtil.getInstance().showSettingsDialog(null, "IntelliGrade Settings");
- }
- });
- }
- artemisClient = artemisInstance;
- }
- return artemisClient;
- }
-
- /**
- * Display an error ballon that indicates a login error.
- *
- * @param msg The message to be displayed. Should describe the login error.
- * @param fix A possible fix for the error. Should be non-null. A null value
- * is only allowed if a fix is provided in the message.
- */
- public static void displayLoginErrorBalloon(String msg, @Nullable AnAction fix) {
- // create Balloon notification indicating error & fix
- Notification balloon = NotificationGroupManager.getInstance()
- .getNotificationGroup("IntelliGrade Notifications")
- .createNotification(msg, NotificationType.ERROR)
- .setTitle(LOGIN_ERROR_DIALOG_TITLE);
- // add fix if available
- if (fix != null) {
- balloon.addAction(fix);
- }
-
- balloon.notify(null);
- }
-
- /**
- * Display an error balloon that indicates a generic error message.
- *
- * @param balloonContent The message to be displayed in the error balloon
- */
- public static void displayGenericErrorBalloon(String balloonContent) {
- NotificationGroupManager.getInstance()
- .getNotificationGroup("IntelliGrade Notifications")
- .createNotification(balloonContent, NotificationType.ERROR)
- .setTitle(ArtemisUtils.GENERIC_ARTEMIS_ERROR_TITLE)
- .notify(null);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/utils/AssessmentUtils.java b/src/main/java/edu/kit/kastel/utils/AssessmentUtils.java
deleted file mode 100644
index 9294d73..0000000
--- a/src/main/java/edu/kit/kastel/utils/AssessmentUtils.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.utils;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicReference;
-
-import edu.kit.kastel.sdq.artemis4j.api.grading.IAnnotation;
-import edu.kit.kastel.sdq.artemis4j.grading.config.ExerciseConfig;
-import edu.kit.kastel.sdq.artemis4j.grading.config.GradingConfig;
-import edu.kit.kastel.sdq.artemis4j.grading.model.annotation.Annotation;
-import edu.kit.kastel.sdq.artemis4j.grading.model.annotation.AnnotationException;
-import edu.kit.kastel.sdq.artemis4j.grading.model.annotation.AnnotationManagement;
-import edu.kit.kastel.state.AssessmentModeHandler;
-import edu.kit.kastel.wrappers.AnnotationWithTextSelection;
-import edu.kit.kastel.wrappers.PlugInEventListener;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Holds all information and meta on assessments that are required globally.
- * Some Information are:
- * - all assessment annotations
- * - whether assessment mode is currently enabled
- */
-public final class AssessmentUtils {
- private static List assesmentListeners = new ArrayList<>();
-
- private static AnnotationManagement annotationManager = new AnnotationManagement();
-
- private static AnnotationWithTextSelection latestAnnotation;
-
- private static GradingConfig config;
-
- private static final String ERROR_GETTING_EXERCISE_CONFIG = "IO Error while obtaining an exercise config";
-
- private AssessmentUtils() {
- throw new IllegalAccessError("Utility Class constructor");
- }
-
- public static void initExerciseConfig(GradingConfig config) {
- AssessmentUtils.config = config;
- }
-
- /**
- * Get the exercise config from the saved grading config or Optional#empty if no lock is currently held
- *
- * @return the exercise config behind the current grading config or Empty if no lock is currently held
- */
- public static Optional getConfigAsExerciseCfg() {
- AtomicReference> returnValue = new AtomicReference<>(Optional.empty());
- // we can only obtain a config if a lock is currently held
- AssessmentModeHandler.getInstance().getAssessmentLock().ifPresent(extendedLockResult -> {
- try {
- returnValue.set(
- Optional.of(AssessmentUtils.config.getExerciseConfig(extendedLockResult.getExercise())));
- } catch (IOException e) {
- ArtemisUtils.displayGenericErrorBalloon(ERROR_GETTING_EXERCISE_CONFIG);
- }
- });
-
- return returnValue.get();
- }
-
- /**
- * add an annotation and update all listening components.
- *
- * @param annotation the annotation to be added
- * @throws AnnotationException if adding the annotation fails
- */
- public static void addAnnotation(@NotNull AnnotationWithTextSelection annotation) throws AnnotationException {
-
- AssessmentUtils.annotationManager.addAnnotation(
- annotation.getUUID(),
- annotation.getMistakeType(),
- annotation.getStartLine(),
- annotation.getEndLine(),
- annotation.getClassFilePath(),
- annotation.getCustomMessage().orElse(""),
- annotation.getCustomPenalty().orElse(0.0));
-
- // add latest annotation so UI can use it
- latestAnnotation = annotation;
-
- // trigger each event so that all assessment views are updated
- assesmentListeners.forEach(PlugInEventListener::trigger);
- }
-
- public static void deleteAnnotation(@NotNull Annotation annotation) {
- AssessmentUtils.annotationManager.removeAnnotation(annotation.getUUID());
- }
-
- public static List getAllAnnotations() {
- return new ArrayList<>(AssessmentUtils.annotationManager.getAnnotations());
- }
-
- public static void resetAnnotations() {
- AssessmentUtils.annotationManager = new AnnotationManagement();
- }
-
- public static void registerAssessmentListener(PlugInEventListener assessmentListener) {
- AssessmentUtils.assesmentListeners.add(assessmentListener);
- }
-
- public static void resetAssessmentListeners() {
- assesmentListeners = new ArrayList<>();
- }
-
- public static AnnotationWithTextSelection getLatestAnnotation() {
- return latestAnnotation;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/wrappers/AnnotationWithTextSelection.java b/src/main/java/edu/kit/kastel/wrappers/AnnotationWithTextSelection.java
deleted file mode 100644
index 176772d..0000000
--- a/src/main/java/edu/kit/kastel/wrappers/AnnotationWithTextSelection.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.wrappers;
-
-import com.intellij.openapi.editor.Editor;
-import com.intellij.openapi.editor.markup.RangeHighlighter;
-import com.intellij.openapi.fileEditor.FileEditorManager;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.project.ProjectManager;
-import edu.kit.kastel.sdq.artemis4j.api.grading.IMistakeType;
-import edu.kit.kastel.sdq.artemis4j.grading.model.annotation.Annotation;
-
-public class AnnotationWithTextSelection extends Annotation {
-
- RangeHighlighter mistakeHighlighter;
-
- public AnnotationWithTextSelection(
- String uuid,
- IMistakeType mistakeType,
- int startLine,
- int endLine,
- String fullyClassifiedClassName,
- String customMessage,
- Double customPenalty,
- RangeHighlighter pMistakeHighlighter) {
- super(uuid, mistakeType, startLine, endLine, fullyClassifiedClassName, customMessage, customPenalty);
- this.mistakeHighlighter = pMistakeHighlighter;
- }
-
- /**
- * Deletes the mistake Highlighter associated with this Annotation
- */
- public void deleteHighlighter() {
- Project currentProject = ProjectManager.getInstance().getOpenProjects()[0];
- Editor editor = FileEditorManager.getInstance(currentProject).getSelectedTextEditor();
-
- editor.getMarkupModel().removeHighlighter(mistakeHighlighter);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/wrappers/Displayable.java b/src/main/java/edu/kit/kastel/wrappers/Displayable.java
deleted file mode 100644
index df1cf65..0000000
--- a/src/main/java/edu/kit/kastel/wrappers/Displayable.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.wrappers;
-
-/**
- * A generic wrapper class to enable overriding the toString method.
- * Required because some UI classes are parameterized and automatically call toString.
- *
- * @param The Type to be wrapped
- */
-public abstract class Displayable {
- private final T item;
-
- protected Displayable(T item) {
- this.item = item;
- }
-
- /**
- * Get the wrapped value.
- *
- * @return A reference to the wrapped value. Does not copy.
- */
- public T getWrappedValue() {
- return item;
- }
-
- @Override
- public abstract String toString();
-}
diff --git a/src/main/java/edu/kit/kastel/wrappers/DisplayableCourse.java b/src/main/java/edu/kit/kastel/wrappers/DisplayableCourse.java
deleted file mode 100644
index 688f5f8..0000000
--- a/src/main/java/edu/kit/kastel/wrappers/DisplayableCourse.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.wrappers;
-
-import edu.kit.kastel.sdq.artemis4j.api.artemis.Course;
-
-/**
- * A course that can be displayed in the UI by calling the toString method.
- */
-public class DisplayableCourse extends Displayable {
-
- public DisplayableCourse(Course item) {
- super(item);
- }
-
- @Override
- public String toString() {
- return String.format(
- "%s (%s)",
- this.getWrappedValue().getTitle(), this.getWrappedValue().getShortName());
- }
-}
diff --git a/src/main/java/edu/kit/kastel/wrappers/DisplayableExam.java b/src/main/java/edu/kit/kastel/wrappers/DisplayableExam.java
deleted file mode 100644
index 2c91228..0000000
--- a/src/main/java/edu/kit/kastel/wrappers/DisplayableExam.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.wrappers;
-
-import edu.kit.kastel.sdq.artemis4j.api.artemis.exam.Exam;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * An exam that can be displayed in the UI by calling the toString method.
- * The wrapped value is Nullable. If exam is null its String representation
- * will be {@value EMPTY_EXAM_REPRESENTATION}.
- */
-public class DisplayableExam extends Displayable {
-
- private static final String EMPTY_EXAM_REPRESENTATION = "No exam selected";
-
- public DisplayableExam(@Nullable Exam exam) {
- super(exam);
- }
-
- @Override
- public String toString() {
- if (this.getWrappedValue() != null) {
- return this.getWrappedValue().getTitle();
- } else {
- return EMPTY_EXAM_REPRESENTATION;
- }
- }
-}
diff --git a/src/main/java/edu/kit/kastel/wrappers/DisplayableExercise.java b/src/main/java/edu/kit/kastel/wrappers/DisplayableExercise.java
deleted file mode 100644
index 38ec3d1..0000000
--- a/src/main/java/edu/kit/kastel/wrappers/DisplayableExercise.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.wrappers;
-
-import edu.kit.kastel.sdq.artemis4j.api.artemis.Exercise;
-
-/**
- * An exercise that can be displayed in the UI by calling the toString method.
- */
-public class DisplayableExercise extends Displayable {
-
- public DisplayableExercise(Exercise exercise) {
- super(exercise);
- }
-
- @Override
- public String toString() {
- return String.format(
- "%s (%s)",
- this.getWrappedValue().getTitle(), this.getWrappedValue().getShortName());
- }
-}
diff --git a/src/main/java/edu/kit/kastel/wrappers/DisplayableMistakeType.java b/src/main/java/edu/kit/kastel/wrappers/DisplayableMistakeType.java
deleted file mode 100644
index 64ea310..0000000
--- a/src/main/java/edu/kit/kastel/wrappers/DisplayableMistakeType.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.wrappers;
-
-import com.intellij.DynamicBundle;
-import edu.kit.kastel.sdq.artemis4j.api.grading.IMistakeType;
-
-public class DisplayableMistakeType extends Displayable {
-
- private static final String LOCALE = DynamicBundle.getLocale().getLanguage();
-
- public DisplayableMistakeType(IMistakeType item) {
- super(item);
- }
-
- @Override
- public String toString() {
- return super.getWrappedValue().getButtonText(LOCALE);
- }
-}
diff --git a/src/main/java/edu/kit/kastel/wrappers/ExtendedLockResult.java b/src/main/java/edu/kit/kastel/wrappers/ExtendedLockResult.java
deleted file mode 100644
index c964714..0000000
--- a/src/main/java/edu/kit/kastel/wrappers/ExtendedLockResult.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.wrappers;
-
-import edu.kit.kastel.sdq.artemis4j.api.ArtemisClientException;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.Exercise;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.assessment.LockResult;
-import edu.kit.kastel.sdq.artemis4j.api.artemis.assessment.Submission;
-import edu.kit.kastel.utils.ArtemisUtils;
-
-public class ExtendedLockResult {
-
- private final Integer submissionId;
- private final Exercise exercise;
-
- private final LockResult submissionLock;
-
- public ExtendedLockResult(Integer submissionId, Exercise exercise, LockResult submissionLock) {
- this.submissionId = submissionId;
- this.exercise = exercise;
- this.submissionLock = submissionLock;
- }
-
- public Integer getLockedSubmissionId() {
- return submissionId;
- }
-
- public Exercise getExercise() {
- return exercise;
- }
-
- public Submission getSubmission() throws ArtemisClientException {
- return ArtemisUtils.getArtemisClientInstance()
- .getSubmissionArtemisClient()
- .getSubmissionById(exercise, submissionId);
- }
-
- public LockResult getSubmissionLock() {
- return submissionLock;
- }
-}
diff --git a/src/main/java/edu/kit/kastel/wrappers/PlugInEventListener.java b/src/main/java/edu/kit/kastel/wrappers/PlugInEventListener.java
deleted file mode 100644
index b1f821f..0000000
--- a/src/main/java/edu/kit/kastel/wrappers/PlugInEventListener.java
+++ /dev/null
@@ -1,10 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.wrappers;
-
-/**
- * represents anything that can react to a plugIn internal event.
- */
-public interface PlugInEventListener extends java.util.EventListener {
-
- void trigger();
-}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 885e2b3..826de5f 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -12,30 +12,33 @@
on how to target different products -->
com.intellij.modules.platform
com.intellij.modules.java
+ org.jetbrains.idea.maven
+ instance="edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettings"
+ id="edu.kit.kastel.sdq.intelligrade.extensions.settings.ArtemisSettings"
+ displayName="Artemis (IntelliGrade)"/>
-
-
+
+
+
+
-
diff --git a/src/main/resources/guiStrings.properties b/src/main/resources/guiStrings.properties
deleted file mode 100644
index e1053c9..0000000
--- a/src/main/resources/guiStrings.properties
+++ /dev/null
@@ -1,37 +0,0 @@
-#
-# Created by JFormDesigner on Thu Jul 13 01:06:42 CEST 2023
-#
-
-AnnotationColor=Color for annotations
-AssesmentViewContent.btnGradingRound2.text=Start Grading Round 2
-AssesmentViewContent.btnGradingRound1.text=Start Grading Round 1
-AssesmentViewContent.btnSaveAssessment.text=Save Assessment
-AssesmentViewContent.submitAssesmentBtn.text=Submit Assessment
-AssesmentViewContent.button3.text=Reload Assessment
-AssesmentViewContent.button4.text=Close Assesment
-AssesmentViewContent.button5.text=Re-run Autograder
-AssesmentViewContent.button6.text=Refresh submissions
-AssesmentViewContent.button7.text=Reload Assessment
-AssesmentViewContent.label1.text=Course
-AssesmentViewContent.label2.text=Exam
-AssesmentViewContent.label3.text=Exercise
-AssesmentViewContent.label4.text=Test Results from Artemis
-AssesmentViewContent.label7.text=Submission
-AssesmentViewContent.label8.text=Statistics\:
-AssesmentViewContent.label9.text=Currently assessing\:
-AssesmentViewContent.GradingPanel.tab.title=Grading
-AssesmentViewContent.TestResultsPanel.tab.title=Test Results
-AssesmentViewContent.panel3.border=Backlog
-AssesmentViewContent.panel5.border=Metrics
-AssesmentViewContent.generalPanel.border=General
-AssesmentViewContent.assessmentPanel.border=Assessment
-DebugMenuContent.btnLogin.text=test Connection
-DebugMenuContent.btnLogout.text=log out
-DebugMenuContent.artemisUrlInput.text=https\://artemis-test.ipd.kit.edu/
-DebugMenuContent.label4.text=Artemis URL
-DebugMenuContent.loggedInLabel.text=false
-DebugMenuContent.label3.text=Logged in\:
-DebugMenuContent.label5.text=Columns per rating group
-DebugMenuContent.label6.text=text
-LabelPwdField=Password
-labelUnameField=Username
diff --git a/src/main/resources/guiStrings_de.properties b/src/main/resources/guiStrings_de.properties
deleted file mode 100644
index 971e1da..0000000
--- a/src/main/resources/guiStrings_de.properties
+++ /dev/null
@@ -1,25 +0,0 @@
-#
-# Created by JFormDesigner on Thu Jul 13 01:07:12 CEST 2023
-#
-
-AnnotationColor=Farbe der Anmerkungen
-AssesmentViewContent.GradingPanel.tab.title=Bewertung
-AssesmentViewContent.btnGradingRound1.text=Neue Erstbewertung
-AssesmentViewContent.btnGradingRound2.text=Neue Zweitbewertung
-AssesmentViewContent.btnSaveAssessment.text=Bewertung speichern
-AssesmentViewContent.submitAssesmentBtn.text=Bewertung abgeben
-AssesmentViewContent.button3.text=Bewertung neu laden
-AssesmentViewContent.button4.text=Bewertung schlieen
-AssesmentViewContent.button5.text=Autograder neu ausfhren
-AssesmentViewContent.label1.text=Kurs
-AssesmentViewContent.label2.text=Klausur
-AssesmentViewContent.label3.text=Aufgabe
-AssesmentViewContent.label4.text=Testergebnisse
-AssesmentViewContent.TestResultsPanel.tab.title=Testergebnisse
-AssesmentViewContent.assessmentPanel.border=Bewertung
-DebugMenuContent.btnLogin.text=Verbindung testen
-DebugMenuContent.btnLogout.text=ausloggen
-DebugMenuContent.label3.text=Eingeloggt?
-DebugMenuContent.loggedInLabel.text=Nein
-LabelPwdField=Passwort
-labelUnameField=Nutzername
diff --git a/src/main/resources/guiStrings_en.properties b/src/main/resources/guiStrings_en.properties
deleted file mode 100644
index 91d8f8b..0000000
--- a/src/main/resources/guiStrings_en.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-#
-# Created by JFormDesigner on Thu Jul 13 01:07:12 CEST 2023
-#
-
-AnnotationColor=Color for annotations
diff --git a/src/main/resources/icons/annotation1.svg b/src/main/resources/icons/annotation1.svg
new file mode 100644
index 0000000..79fe4d7
--- /dev/null
+++ b/src/main/resources/icons/annotation1.svg
@@ -0,0 +1,78 @@
+
+
diff --git a/src/main/resources/icons/annotation2.svg b/src/main/resources/icons/annotation2.svg
new file mode 100644
index 0000000..c885d9a
--- /dev/null
+++ b/src/main/resources/icons/annotation2.svg
@@ -0,0 +1,99 @@
+
+
diff --git a/src/main/resources/icons/annotation3.svg b/src/main/resources/icons/annotation3.svg
new file mode 100644
index 0000000..5ce0db1
--- /dev/null
+++ b/src/main/resources/icons/annotation3.svg
@@ -0,0 +1,99 @@
+
+
diff --git a/src/main/resources/icons/annotation4.svg b/src/main/resources/icons/annotation4.svg
new file mode 100644
index 0000000..3943dff
--- /dev/null
+++ b/src/main/resources/icons/annotation4.svg
@@ -0,0 +1,109 @@
+
+
diff --git a/src/test/java/edu/kit/kastel/ArchitectureTest.java b/src/test/java/edu/kit/kastel/ArchitectureTest.java
new file mode 100644
index 0000000..29f8604
--- /dev/null
+++ b/src/test/java/edu/kit/kastel/ArchitectureTest.java
@@ -0,0 +1,27 @@
+/* Licensed under EPL-2.0 2024. */
+package edu.kit.kastel;
+
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.lang.ArchRule;
+
+@AnalyzeClasses(packages = "edu.kit.kastel.sdq.intelligrade")
+class ArchitectureTest {
+ @ArchTest
+ static final ArchRule noForEachInCollectionsOrStream = noClasses()
+ .should()
+ .callMethod(Stream.class, "forEach", Consumer.class)
+ .orShould()
+ .callMethod(Stream.class, "forEachOrdered", Consumer.class)
+ .orShould()
+ .callMethod(List.class, "forEach", Consumer.class)
+ .orShould()
+ .callMethod(List.class, "forEachOrdered", Consumer.class)
+ .because("Lambdas should be functional. ForEach is typically used for side-effects.");
+}
diff --git a/src/test/java/edu/kit/kastel/extensions/settings/ArtemisSettingsStateTest.java b/src/test/java/edu/kit/kastel/extensions/settings/ArtemisSettingsStateTest.java
deleted file mode 100644
index cf845ff..0000000
--- a/src/test/java/edu/kit/kastel/extensions/settings/ArtemisSettingsStateTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/* Licensed under EPL-2.0 2024. */
-package edu.kit.kastel.extensions.settings;
-
-import com.intellij.testFramework.fixtures.BasePlatformTestCase;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-class ArtemisSettingsStateTest extends BasePlatformTestCase {
-
- @Test
- void alwaysTrueTest() {
- Assertions.assertTrue(true);
- }
-}