diff --git a/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsListPanel.java b/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsListPanel.java new file mode 100644 index 0000000..c6709c4 --- /dev/null +++ b/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsListPanel.java @@ -0,0 +1,78 @@ +package edu.kit.kastel.extensions.guis; + +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.ui.ScrollPaneFactory; +import com.intellij.ui.table.JBTable; +import edu.kit.kastel.state.PluginState; +import edu.kit.kastel.utils.EditorUtil; + +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Arrays; + +public class AnnotationsListPanel extends SimpleToolWindowPanel { + private JBTable table; + private AnnotationsTableModel model; + + public AnnotationsListPanel() { + super(true, true); + + model = new AnnotationsTableModel(); + table = new JBTable(model); + setContent(ScrollPaneFactory.createScrollPane(table)); + + 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()) + .filter(row -> row >= 0) + .mapToObj(model::get) + .toList(); + + var assessment = PluginState.getInstance().getActiveAssessment().orElseThrow(); + for (var annotation : annotationsToDelete) { + assessment.deleteAnnotation(annotation); + } + } + } + }); + table.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + int row = table.rowAtPoint(e.getPoint()); + if (e.getClickCount() == 2 && row >= 0) { + var annotation = model.get(row); + + var file = EditorUtil.getAnnotationFile(annotation); + var document = FileDocumentManager.getInstance().getDocument(file); + int offset = document.getLineStartOffset(annotation.getStartLine()); + FileEditorManager.getInstance(EditorUtil.getActiveProject()).openTextEditor( + new OpenFileDescriptor(EditorUtil.getActiveProject(), file, offset), + true); + } + } + }); + + PluginState.getInstance() + .registerAssessmentStartedListener( + assessment -> assessment.registerAnnotationsUpdatedListener(annotations -> { + AnnotationsTableModel model = ((AnnotationsTableModel) table.getModel()); + model.setAnnotations(annotations); + table.updateUI(); + })); + + PluginState.getInstance().registerAssessmentClosedListener(() -> { + AnnotationsTableModel model = ((AnnotationsTableModel) table.getModel()); + model.clearAnnotations(); + table.updateUI(); + }); + } +} diff --git a/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsTableModel.java b/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsTableModel.java index 8a2e077..ae6a672 100644 --- a/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsTableModel.java +++ b/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsTableModel.java @@ -1,16 +1,14 @@ /* Licensed under EPL-2.0 2024. */ package edu.kit.kastel.extensions.guis; +import com.intellij.DynamicBundle; +import edu.kit.kastel.sdq.artemis4j.grading.Annotation; + +import javax.swing.table.AbstractTableModel; 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; -import edu.kit.kastel.state.PluginState; - /** * The table model for the annotations table. */ @@ -68,9 +66,8 @@ public void clearAnnotations() { this.annotations.clear(); } - public void deleteItem(int itemIndex) { - Annotation annotation = this.annotations.get(itemIndex); - PluginState.getInstance().getActiveAssessment().orElseThrow().deleteAnnotation(annotation); + public Annotation get(int index) { + return annotations.get(index); } private String formatLines(Annotation annotation) { diff --git a/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsViewContent.java b/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsViewContent.java deleted file mode 100644 index 71e3817..0000000 --- a/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsViewContent.java +++ /dev/null @@ -1,84 +0,0 @@ -/* Licensed under EPL-2.0 2023-2024. */ -package edu.kit.kastel.extensions.guis; - -import java.awt.event.*; - -import javax.swing.*; - -import com.intellij.ui.components.*; -import com.intellij.ui.table.*; -import edu.kit.kastel.state.PluginState; - -/** - * @author clemens - */ -public class AnnotationsViewContent extends JPanel { - public AnnotationsViewContent() { - initComponents(); - PluginState.getInstance() - .registerAssessmentStartedListener( - assessment -> assessment.registerAnnotationsUpdatedListener(annotations -> { - AnnotationsTableModel model = ((AnnotationsTableModel) this.annotationsTable.getModel()); - model.setAnnotations(annotations); - this.annotationsTable.updateUI(); - })); - - PluginState.getInstance().registerAssessmentClosedListener(() -> { - AnnotationsTableModel model = ((AnnotationsTableModel) this.annotationsTable.getModel()); - model.clearAnnotations(); - this.annotationsTable.updateUI(); - }); - } - - private void createUIComponents() { - // TODO: add custom component creation code here - } - - private void annotationsTableKeyReleased(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_DELETE) { - int selectedRow = annotationsTable.getSelectedRow(); - if (selectedRow != -1) { - ((AnnotationsTableModel) this.annotationsTable.getModel()).deleteItem(selectedRow); - this.updateUI(); - } - } - } - - private void initComponents() { - // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off - // Generated using JFormDesigner Evaluation license - FooBar - scrollPane1 = new JBScrollPane(); - annotationsTable = new JBTable(new AnnotationsTableModel()); - - //======== 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 BoxLayout(this, BoxLayout.X_AXIS)); - - //======== scrollPane1 ======== - { - - //---- annotationsTable ---- - annotationsTable.addKeyListener(new KeyAdapter() { - @Override - public void keyReleased(KeyEvent e) { - annotationsTableKeyReleased(e); - } - }); - scrollPane1.setViewportView(annotationsTable); - } - add(scrollPane1); - // 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 - FooBar - private JBScrollPane scrollPane1; - private JBTable annotationsTable; - // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on -} diff --git a/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsViewContent.jfd b/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsViewContent.jfd deleted file mode 100644 index d0146d6..0000000 --- a/src/main/java/edu/kit/kastel/extensions/guis/AnnotationsViewContent.jfd +++ /dev/null @@ -1,25 +0,0 @@ -JFDML JFormDesigner: "8.2.0.0.331" Java: "17.0.9" encoding: "UTF-8" - -new FormModel { - contentType: "form/swing" - root: new FormRoot { - add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class javax.swing.BoxLayout ) { - "axis": 0 - } ) { - name: "this" - add( new FormContainer( "com.intellij.ui.components.JBScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "scrollPane1" - add( new FormComponent( "com.intellij.ui.table.JBTable" ) { - name: "annotationsTable" - auxiliary() { - "JavaCodeGenerator.customCreateCode": "new JBTable(new AnnotationsTableModel());" - } - addEvent( new FormEvent( "java.awt.event.KeyListener", "keyReleased", "annotationsTableKeyReleased", true ) ) - } ) - } ) - }, new FormLayoutConstraints( null ) { - "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 955, 300 ) - } ) - } -} diff --git a/src/main/java/edu/kit/kastel/extensions/guis/AssessmentPanel.java b/src/main/java/edu/kit/kastel/extensions/guis/AssessmentPanel.java new file mode 100644 index 0000000..061e783 --- /dev/null +++ b/src/main/java/edu/kit/kastel/extensions/guis/AssessmentPanel.java @@ -0,0 +1,111 @@ +package edu.kit.kastel.extensions.guis; + +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.extensions.settings.ArtemisSettingsState; +import edu.kit.kastel.sdq.artemis4j.grading.penalty.MistakeType; +import edu.kit.kastel.sdq.artemis4j.grading.penalty.RatingGroup; +import edu.kit.kastel.state.ActiveAssessment; +import edu.kit.kastel.state.PluginState; +import net.miginfocom.swing.MigLayout; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.border.TitledBorder; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.util.IdentityHashMap; +import java.util.Locale; +import java.util.Map; + +public class AssessmentPanel extends SimpleToolWindowPanel { + private static final Locale LOCALE = DynamicBundle.getLocale(); + + private final JPanel content; + private final Map ratingGroupBorders = new IdentityHashMap<>(); + private final Map mistakeTypeButtons = new IdentityHashMap<>(); + + public AssessmentPanel() { + super(true, true); + + content = new JBPanel<>(new MigLayout("wrap 1", "[grow]")); + setContent(ScrollPaneFactory.createScrollPane(content)); + + this.showNoActiveAssessment(); + PluginState.getInstance().registerAssessmentStartedListener(this::createMistakeButtons); + PluginState.getInstance().registerAssessmentClosedListener(this::showNoActiveAssessment); + } + + private void createMistakeButtons(ActiveAssessment assessment) { + content.removeAll(); + + int buttonsPerRow = ArtemisSettingsState.getInstance().getColumnsPerRatingGroup(); + for (var ratingGroup : assessment.getGradingConfig().getRatingGroups()) { + if (ratingGroup.getMistakeTypes().isEmpty()) { + continue; + } + + var panel = new JBPanel<>(new MigLayout("wrap " + buttonsPerRow)); + + var border = BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(JBColor.border()), + String.format(getRatingGroupTitle(assessment, 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)); + + if (mistakeType.getReporting().shouldScore()) { + button.setForeground(JBColor.GREEN); + } else { + button.setBackground(JBColor.RED); + } + + button.addActionListener(a -> assessment.addAnnotationAtCaret(mistakeType, (a.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK)); + panel.add(button); + this.mistakeTypeButtons.put(mistakeType, button); + } + + this.content.add(panel, "growx"); + } + + assessment.registerAnnotationsUpdatedListener(annotations -> { + // Update rating group titles + for (var entry : this.ratingGroupBorders.entrySet()) { + var ratingGroup = entry.getKey(); + var border = entry.getValue(); + border.setTitle(getRatingGroupTitle(assessment, ratingGroup)); + } + }); + + this.updateUI(); + } + + private void showNoActiveAssessment() { + this.ratingGroupBorders.clear(); + this.mistakeTypeButtons.clear(); + + content.removeAll(); + content.add(new JBLabel("No active assessment"), "growx"); + this.updateUI(); + } + + private String getRatingGroupTitle(ActiveAssessment assessment, RatingGroup ratingGroup) { + var points = assessment.getAssessment().calculatePointsForRatingGroup(ratingGroup); + return "%s (%.1f of [%.1f, %.1f])".formatted( + ratingGroup.getDisplayName().translateTo(LOCALE), + points.score(), + ratingGroup.getMinPenalty(), + ratingGroup.getMaxPenalty() + ); + } +} diff --git a/src/main/java/edu/kit/kastel/extensions/guis/AssessmentViewContent.java b/src/main/java/edu/kit/kastel/extensions/guis/AssessmentViewContent.java deleted file mode 100644 index 001b8ca..0000000 --- a/src/main/java/edu/kit/kastel/extensions/guis/AssessmentViewContent.java +++ /dev/null @@ -1,415 +0,0 @@ -/* Licensed under EPL-2.0 2023-2024. */ -package edu.kit.kastel.extensions.guis; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.util.ResourceBundle; - -import javax.swing.BorderFactory; -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; -import javax.swing.SwingConstants; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.LineBorder; -import javax.swing.border.TitledBorder; - -import com.intellij.openapi.ui.ComboBox; -import com.intellij.openapi.ui.TextFieldWithBrowseButton; -import com.intellij.ui.components.JBLabel; -import com.intellij.ui.components.JBScrollPane; -import com.intellij.ui.components.JBTabbedPane; -import com.intellij.ui.table.JBTable; -import com.intellij.uiDesigner.core.GridConstraints; -import com.intellij.uiDesigner.core.GridLayoutManager; -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 net.miginfocom.swing.MigLayout; - -/** - * @author clemens - */ -public class AssessmentViewContent extends JPanel { - public AssessmentViewContent() { - initComponents(); - } - - public ComboBox getCoursesDropdown() { - return coursesDropdown; - } - - public ComboBox getExamsDropdown() { - return examsDropdown; - } - - public ComboBox getExercisesDropdown() { - return exercisesDropdown; - } - - public ComboBox getBacklogSelector() { - return backlogSelector; - } - - public TextFieldWithBrowseButton getGradingConfigPathInput() { - return gradingConfigPathInput; - } - - public JButton getBtnGradingRound1() { - return btnGradingRound1; - } - - public JPanel getRatingGroupContainer() { - return ratingGroupContainer; - } - - public JButton getBtnSaveAssessment() { - return btnSaveAssessment; - } - - public JButton getSubmitAssesmentBtn() { - return submitAssesmentBtn; - } - - public JBLabel getAssessmentModeLabel() { - return assessmentModeLabel; - } - - public StatisticsContainer getStatisticsContainer() { - return statisticsContainer; - } - - private void createUIComponents() { - // TODO: add custom component creation code here - } - - private void initComponents() { - // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off - // Generated using JFormDesigner Evaluation license - FooBar - ResourceBundle bundle = ResourceBundle.getBundle("guiStrings"); - var tabbedPane1 = new JBTabbedPane(); - scrollPane2 = new JBScrollPane(); - var AssessmentPanel = new JPanel(); - var label1 = new JLabel(); - coursesDropdown = new ComboBox<>(); - var label2 = new JLabel(); - examsDropdown = new ComboBox<>(); - var label3 = new JLabel(); - exercisesDropdown = new ComboBox<>(); - var label5 = new JLabel(); - gradingConfigPathInput = new TextFieldWithBrowseButton(); - var separator1 = new JSeparator(); - var generalPanel = new JPanel(); - btnGradingRound1 = new JButton(); - btnGradingRound2 = new JButton(); - button5 = new JButton(); - var assessmentPanel = new JPanel(); - btnSaveAssessment = new JButton(); - submitAssesmentBtn = new JButton(); - button3 = new JButton(); - button4 = new JButton(); - var panel5 = new JPanel(); - var label8 = new JBLabel(); - statisticsContainer = new StatisticsContainer(); - label9 = new JBLabel(); - assessmentModeLabel = new JBLabel(); - var panel3 = new JPanel(); - var label7 = new JLabel(); - backlogSelector = new ComboBox<>(); - panel4 = new JPanel(); - button6 = new JButton(); - button7 = new JButton(); - GradingPanel = new JPanel(); - scrollPane = new JScrollPane(); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - ratingGroupContainer = new JPanel(); - TestResultsPanel = new JPanel(); - var label4 = new JLabel(); - var scrollPane1 = new JBScrollPane(); - testResultsTable = new JBTable(); - - //======== this ======== - setBorder (new javax. swing. border. CompoundBorder( new javax .swing .border .TitledBorder (new - javax. swing. border. EmptyBorder( 0, 0, 0, 0) , "JF\u006frmDes\u0069gner \u0045valua\u0074ion", javax - . swing. border. TitledBorder. CENTER, javax. swing. border. TitledBorder. BOTTOM, new java - .awt .Font ("D\u0069alog" ,java .awt .Font .BOLD ,12 ), java. awt - . Color. red) , getBorder( )) ); addPropertyChangeListener (new java. beans. - PropertyChangeListener( ){ @Override public void propertyChange (java .beans .PropertyChangeEvent e) {if ("\u0062order" . - equals (e .getPropertyName () )) throw new RuntimeException( ); }} ); - setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); - - //======== tabbedPane1 ======== - { - - //======== scrollPane2 ======== - { - - //======== AssessmentPanel ======== - { - AssessmentPanel.setLayout(new MigLayout( - "fillx,insets 0,hidemode 3,align left top,gap 5 5", - // columns - "[80:115,fill]" + - "[151,grow,fill]", - // rows - "[]" + - "[]" + - "[]" + - "[30]" + - "[10:10]" + - "[]" + - "[]" + - "[]" + - "[]" + - "[]")); - - //---- label1 ---- - label1.setText(bundle.getString("AssesmentViewContent.label1.text")); - AssessmentPanel.add(label1, "pad 0,cell 0 0,alignx label,growx 0"); - AssessmentPanel.add(coursesDropdown, "cell 1 0"); - - //---- label2 ---- - label2.setText(bundle.getString("AssesmentViewContent.label2.text")); - AssessmentPanel.add(label2, "cell 0 1,alignx label,growx 0"); - AssessmentPanel.add(examsDropdown, "cell 1 1"); - - //---- label3 ---- - label3.setText(bundle.getString("AssesmentViewContent.label3.text")); - AssessmentPanel.add(label3, "cell 0 2,alignx label,growx 0"); - AssessmentPanel.add(exercisesDropdown, "cell 1 2"); - - //---- label5 ---- - label5.setText("Grading config"); - AssessmentPanel.add(label5, "cell 0 3,alignx label,growx 0"); - - //---- gradingConfigPathInput ---- - gradingConfigPathInput.setEditable(false); - AssessmentPanel.add(gradingConfigPathInput, "cell 1 3"); - AssessmentPanel.add(separator1, "cell 0 4 2 1"); - - //======== generalPanel ======== - { - generalPanel.setBorder(new CompoundBorder( - new TitledBorder(new LineBorder(Color.darkGray, 1, true), bundle.getString("AssesmentViewContent.generalPanel.border")), - new EmptyBorder(5, 5, 5, 5))); - generalPanel.setForeground(Color.blue); - generalPanel.setLayout(new BorderLayout()); - - //---- btnGradingRound1 ---- - btnGradingRound1.setText(bundle.getString("AssesmentViewContent.btnGradingRound1.text")); - generalPanel.add(btnGradingRound1, BorderLayout.CENTER); - - //---- btnGradingRound2 ---- - btnGradingRound2.setText(bundle.getString("AssesmentViewContent.btnGradingRound2.text")); - generalPanel.add(btnGradingRound2, BorderLayout.NORTH); - - //---- button5 ---- - button5.setText(bundle.getString("AssesmentViewContent.button5.text")); - generalPanel.add(button5, BorderLayout.SOUTH); - } - AssessmentPanel.add(generalPanel, "cell 0 5 2 1,growx"); - - //======== assessmentPanel ======== - { - assessmentPanel.setBorder(new CompoundBorder( - new TitledBorder(new LineBorder(Color.darkGray, 1, true), bundle.getString("AssesmentViewContent.assessmentPanel.border")), - new EmptyBorder(5, 5, 5, 5))); - assessmentPanel.setLayout(new GridBagLayout()); - ((GridBagLayout)assessmentPanel.getLayout()).columnWidths = new int[] {0, 0, 0}; - ((GridBagLayout)assessmentPanel.getLayout()).rowHeights = new int[] {0, 0, 0}; - ((GridBagLayout)assessmentPanel.getLayout()).columnWeights = new double[] {1.0, 1.0, 1.0E-4}; - ((GridBagLayout)assessmentPanel.getLayout()).rowWeights = new double[] {0.0, 0.0, 1.0E-4}; - - //---- btnSaveAssessment ---- - btnSaveAssessment.setText(bundle.getString("AssesmentViewContent.btnSaveAssessment.text")); - assessmentPanel.add(btnSaveAssessment, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 5, 5), 0, 0)); - - //---- submitAssesmentBtn ---- - submitAssesmentBtn.setText(bundle.getString("AssesmentViewContent.submitAssesmentBtn.text")); - assessmentPanel.add(submitAssesmentBtn, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 5, 0), 0, 0)); - - //---- button3 ---- - button3.setText(bundle.getString("AssesmentViewContent.button3.text")); - assessmentPanel.add(button3, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 5), 0, 0)); - - //---- button4 ---- - button4.setText(bundle.getString("AssesmentViewContent.button4.text")); - assessmentPanel.add(button4, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); - } - AssessmentPanel.add(assessmentPanel, "cell 0 6 2 1"); - - //======== panel5 ======== - { - panel5.setBorder(new TitledBorder(new LineBorder(Color.darkGray, 1, true), bundle.getString("AssesmentViewContent.panel5.border"))); - panel5.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), 0, 0)); - - //---- label8 ---- - label8.setText(bundle.getString("AssesmentViewContent.label8.text")); - panel5.add(label8, new GridConstraints(0, 0, 1, 1, - GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - null, null, null)); - panel5.add(statisticsContainer, new GridConstraints(0, 1, 1, 1, - GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - null, null, null)); - - //---- label9 ---- - label9.setText(bundle.getString("AssesmentViewContent.label9.text")); - panel5.add(label9, new GridConstraints(1, 0, 1, 1, - GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - null, null, null)); - - //---- assessmentModeLabel ---- - assessmentModeLabel.setText("\u274c"); - assessmentModeLabel.setIcon(null); - assessmentModeLabel.setHorizontalAlignment(SwingConstants.LEFT); - panel5.add(assessmentModeLabel, new GridConstraints(1, 1, 1, 1, - GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - null, null, null)); - } - AssessmentPanel.add(panel5, "pad 0,cell 0 7 2 1,growx"); - - //======== panel3 ======== - { - panel3.setBorder(new TitledBorder(new LineBorder(Color.darkGray), bundle.getString("AssesmentViewContent.panel3.border"))); - panel3.setLayout(new GridBagLayout()); - ((GridBagLayout)panel3.getLayout()).columnWidths = new int[] {85, 0, 0}; - ((GridBagLayout)panel3.getLayout()).rowHeights = new int[] {0, 0, 0}; - ((GridBagLayout)panel3.getLayout()).columnWeights = new double[] {0.0, 1.0, 1.0E-4}; - ((GridBagLayout)panel3.getLayout()).rowWeights = new double[] {0.0, 0.0, 1.0E-4}; - - //---- label7 ---- - label7.setText(bundle.getString("AssesmentViewContent.label7.text")); - panel3.add(label7, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.EAST, GridBagConstraints.VERTICAL, - new Insets(0, 0, 5, 5), 0, 0)); - panel3.add(backlogSelector, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 5, 0), 0, 0)); - - //======== panel4 ======== - { - panel4.setLayout(new FlowLayout()); - - //---- button6 ---- - button6.setText(bundle.getString("AssesmentViewContent.button6.text")); - panel4.add(button6); - - //---- button7 ---- - button7.setText(bundle.getString("AssesmentViewContent.button7.text")); - panel4.add(button7); - } - panel3.add(panel4, new GridBagConstraints(0, 1, 2, 1, 0.0, 0.0, - GridBagConstraints.CENTER, GridBagConstraints.BOTH, - new Insets(0, 0, 0, 0), 0, 0)); - } - AssessmentPanel.add(panel3, "cell 0 8 2 1"); - } - scrollPane2.setViewportView(AssessmentPanel); - } - tabbedPane1.addTab(bundle.getString("AssesmentViewContent.assessmentPanel.border"), scrollPane2); - - //======== GradingPanel ======== - { - GradingPanel.setLayout(new MigLayout( - "fillx,hidemode 3,align left top", - // columns - "[fill]", - // rows - "[grow]")); - - //======== scrollPane ======== - { - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - - //======== ratingGroupContainer ======== - { - ratingGroupContainer.setLayout(new BoxLayout(ratingGroupContainer, BoxLayout.Y_AXIS)); - } - scrollPane.setViewportView(ratingGroupContainer); - } - GradingPanel.add(scrollPane, "cell 0 0,growy"); - } - tabbedPane1.addTab(bundle.getString("AssesmentViewContent.GradingPanel.tab.title"), GradingPanel); - - //======== TestResultsPanel ======== - { - TestResultsPanel.setLayout(new MigLayout( - "fillx,hidemode 3,align left top", - // columns - "[fill]" + - "[fill]", - // rows - "[36]" + - "[grow]")); - - //---- label4 ---- - label4.setText(bundle.getString("AssesmentViewContent.label4.text")); - label4.setFont(label4.getFont().deriveFont(label4.getFont().getStyle() | Font.BOLD)); - TestResultsPanel.add(label4, "cell 0 0 2 1"); - - //======== scrollPane1 ======== - { - scrollPane1.setViewportView(testResultsTable); - } - TestResultsPanel.add(scrollPane1, "cell 0 1 2 1,growy"); - } - tabbedPane1.addTab(bundle.getString("AssesmentViewContent.TestResultsPanel.tab.title"), TestResultsPanel); - } - add(tabbedPane1); - // 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 - FooBar - private JBScrollPane scrollPane2; - private ComboBox coursesDropdown; - private ComboBox examsDropdown; - private ComboBox exercisesDropdown; - private TextFieldWithBrowseButton gradingConfigPathInput; - private JButton btnGradingRound1; - private JButton btnGradingRound2; - private JButton button5; - private JButton btnSaveAssessment; - private JButton submitAssesmentBtn; - private JButton button3; - private JButton button4; - private StatisticsContainer statisticsContainer; - private JBLabel label9; - private JBLabel assessmentModeLabel; - private ComboBox backlogSelector; - private JPanel panel4; - private JButton button6; - private JButton button7; - private JPanel GradingPanel; - private JScrollPane scrollPane; - private JPanel ratingGroupContainer; - private JPanel TestResultsPanel; - private JBTable testResultsTable; - // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on -} diff --git a/src/main/java/edu/kit/kastel/extensions/guis/AssessmentViewContent.jfd b/src/main/java/edu/kit/kastel/extensions/guis/AssessmentViewContent.jfd deleted file mode 100644 index efa2b0d..0000000 --- a/src/main/java/edu/kit/kastel/extensions/guis/AssessmentViewContent.jfd +++ /dev/null @@ -1,346 +0,0 @@ -JFDML JFormDesigner: "8.2.4.0.393" Java: "17.0.10" encoding: "UTF-8" - -new FormModel { - "i18n.bundleName": "guiStrings" - "i18n.keyPrefix": "AssesmentViewContent" - "i18n.autoExternalize": true - contentType: "form/swing" - root: new FormRoot { - add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class javax.swing.BoxLayout ) ) { - name: "this" - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - add( new FormContainer( "com.intellij.ui.components.JBTabbedPane", new FormLayoutManager( class javax.swing.JTabbedPane ) ) { - name: "tabbedPane1" - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - add( new FormContainer( "com.intellij.ui.components.JBScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "scrollPane2" - add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { - "$layoutConstraints": "fillx,insets 0,hidemode 3,align left top,gap 5 5" - "$columnConstraints": "[80:115,fill][151,grow,fill]" - "$rowConstraints": "[][][][30][10:10][][][][][]" - } ) { - name: "AssessmentPanel" - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - add( new FormComponent( "javax.swing.JLabel" ) { - name: "label1" - "text": new FormMessage( null, "AssesmentViewContent.label1.text" ) - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "pad 0,cell 0 0,alignx label,growx 0" - } ) - add( new FormComponent( "com.intellij.openapi.ui.ComboBox" ) { - name: "coursesDropdown" - auxiliary() { - "JavaCodeGenerator.typeParameters": "Course" - "JavaCodeGenerator.variableGetter": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 0" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "label2" - "text": new FormMessage( null, "AssesmentViewContent.label2.text" ) - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 1,alignx label,growx 0" - } ) - add( new FormComponent( "com.intellij.openapi.ui.ComboBox" ) { - name: "examsDropdown" - auxiliary() { - "JavaCodeGenerator.typeParameters": "Exam" - "JavaCodeGenerator.variableGetter": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "label3" - "text": new FormMessage( null, "AssesmentViewContent.label3.text" ) - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2,alignx label,growx 0" - } ) - add( new FormComponent( "com.intellij.openapi.ui.ComboBox" ) { - name: "exercisesDropdown" - auxiliary() { - "JavaCodeGenerator.typeParameters": "ProgrammingExercise" - "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.JSeparator" ) { - name: "separator1" - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4 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 5 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 6 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 7 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" - auxiliary() { - "JavaCodeGenerator.typeParameters": "ProgrammingSubmission" - } - }, 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 8 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/ExercisePanel.java b/src/main/java/edu/kit/kastel/extensions/guis/ExercisePanel.java index 58f7744..06e800f 100644 --- a/src/main/java/edu/kit/kastel/extensions/guis/ExercisePanel.java +++ b/src/main/java/edu/kit/kastel/extensions/guis/ExercisePanel.java @@ -1,28 +1,45 @@ package edu.kit.kastel.extensions.guis; +import com.intellij.icons.AllIcons; +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.components.JBLabel; import com.intellij.ui.components.JBPanel; -import edu.kit.kastel.sdq.artemis4j.ArtemisClientException; +import edu.kit.kastel.extensions.settings.ArtemisSettingsState; import edu.kit.kastel.sdq.artemis4j.ArtemisNetworkException; 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.state.PluginState; import edu.kit.kastel.utils.ArtemisUtils; +import edu.kit.kastel.utils.EditorUtil; import net.miginfocom.swing.MigLayout; +import org.jetbrains.annotations.NotNull; import javax.swing.JButton; import javax.swing.JPanel; +import javax.swing.border.LineBorder; import javax.swing.border.TitledBorder; +import javax.swing.event.DocumentEvent; import java.awt.event.ItemEvent; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Comparator; public class ExercisePanel extends SimpleToolWindowPanel { + private static final Logger LOG = Logger.getInstance(ExercisePanel.class); + private final JPanel content = new JBPanel<>(new MigLayout("wrap 2", "[][grow]")); private ComboBox courseSelector; @@ -35,10 +52,10 @@ public class ExercisePanel extends SimpleToolWindowPanel { private JButton startGradingRound2Button; private JPanel assessmentPanel; - private JButton saveAssessmentButton; private JButton submitAssessmentButton; - private JButton reloadAssessmentButton; private JButton cancelAssessmentButton; + private JButton saveAssessmentButton; + private JButton closeAssessmentButton; private JButton reRunAutograder; private JPanel backlogPanel; @@ -62,6 +79,13 @@ public ExercisePanel() { content.add(new JBLabel("Grading Config:")); 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()); + } + }); content.add(gradingConfigPathInput, "growx"); createGeneralPanel(); @@ -76,6 +100,7 @@ public ExercisePanel() { setContent(ScrollPaneFactory.createScrollPane(content)); exerciseSelector.addItemListener(e -> { + // Exercise selected: Update plugin state, enable/disable grading buttons, update backlog if (e.getStateChange() != ItemEvent.DESELECTED) { var exercise = (ProgrammingExercise) e.getItem(); startGradingRound2Button.setEnabled(exercise.hasSecondCorrectionRound()); @@ -89,6 +114,8 @@ public ExercisePanel() { }); examSelector.addItemListener(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) { try { exerciseSelector.removeAllItems(); @@ -103,14 +130,16 @@ public ExercisePanel() { courseSelector.getItem().getProgrammingExercises().forEach(exerciseSelector::addItem); } updateUI(); - - } catch (ArtemisClientException ex) { - ArtemisUtils.displayGenericErrorBalloon("Failed to fetch exercise info: " + ex.getMessage()); + } catch (ArtemisNetworkException ex) { + LOG.error(ex); + ArtemisUtils.displayNetworkErrorBalloon("Failed to fetch exercise info", ex); } } }); courseSelector.addItemListener(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(); @@ -118,52 +147,48 @@ public ExercisePanel() { examSelector.addItem(new OptionalExam(null)); course.getExams().forEach(exam -> examSelector.addItem(new OptionalExam(exam))); updateUI(); - } catch (ArtemisClientException ex) { - ArtemisUtils.displayGenericErrorBalloon("Failed to fetch exam info: " + ex.getMessage()); + } catch (ArtemisNetworkException ex) { + LOG.error(ex); + ArtemisUtils.displayNetworkErrorBalloon("Failed to fetch exam info", ex); } } }); PluginState.getInstance().registerConnectedListener(() -> { + // When a connection is established, update the course selector with the courses of the connection try { courseSelector.removeAllItems(); PluginState.getInstance().getConnection().get().getCourses().forEach(courseSelector::addItem); updateUI(); - } catch (ArtemisClientException ex) { - ArtemisUtils.displayGenericErrorBalloon("Failed to fetch course info: " + ex.getMessage()); + } catch (ArtemisNetworkException ex) { + LOG.error(ex); + ArtemisUtils.displayNetworkErrorBalloon("Failed to fetch course info", ex); } }); PluginState.getInstance().registerAssessmentStartedListener(assessment -> { assessmentPanel.setEnabled(true); - saveAssessmentButton.setEnabled(true); submitAssessmentButton.setEnabled(true); - reloadAssessmentButton.setEnabled(true); cancelAssessmentButton.setEnabled(!assessment.getAssessment().getSubmission().isSubmitted()); + saveAssessmentButton.setEnabled(true); + closeAssessmentButton.setEnabled(true); reRunAutograder.setEnabled(true); }); PluginState.getInstance().registerAssessmentClosedListener(() -> { assessmentPanel.setEnabled(false); - saveAssessmentButton.setEnabled(false); submitAssessmentButton.setEnabled(false); - reloadAssessmentButton.setEnabled(false); cancelAssessmentButton.setEnabled(false); + saveAssessmentButton.setEnabled(false); + closeAssessmentButton.setEnabled(false); reRunAutograder.setEnabled(false); updateBacklog(); }); } - private record OptionalExam(Exam exam) { - @Override - public String toString() { - return exam == null ? "" : exam.toString(); - } - } - private void createGeneralPanel() { - generalPanel = new JBPanel<>().withBorder(new TitledBorder("General")); + generalPanel = new JBPanel<>().withBorder(new TitledBorder(new LineBorder(JBColor.border()), "General")); generalPanel.setLayout(new MigLayout("wrap 1", "[grow]")); startGradingRound1Button = new JButton("Start Grading Round 1"); @@ -177,39 +202,66 @@ private void createGeneralPanel() { private void createAssessmentPanel() { assessmentPanel = new JBPanel<>(new MigLayout("wrap 2", "[grow][grow]")) - .withBorder(new TitledBorder("Assessment")); + .withBorder(new TitledBorder(new LineBorder(JBColor.border()), "Assessment")); assessmentPanel.setEnabled(false); - saveAssessmentButton = new JButton("Save Assessment"); - saveAssessmentButton.setEnabled(false); - saveAssessmentButton.addActionListener(a -> PluginState.getInstance().saveAssessment()); - assessmentPanel.add(saveAssessmentButton, "growx"); - submitAssessmentButton = new JButton("Submit Assessment"); + submitAssessmentButton.setForeground(JBColor.GREEN); submitAssessmentButton.setEnabled(false); submitAssessmentButton.addActionListener(a -> PluginState.getInstance().submitAssessment()); assessmentPanel.add(submitAssessmentButton, "growx"); - reloadAssessmentButton = new JButton("Reload Assessment"); - reloadAssessmentButton.setEnabled(false); - assessmentPanel.add(reloadAssessmentButton, "growx"); - cancelAssessmentButton = new JButton("Cancel Assessment"); cancelAssessmentButton.setEnabled(false); - cancelAssessmentButton.addActionListener(a -> PluginState.getInstance().cancelAssessment()); + 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 assessment will be discarded, 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); assessmentPanel.add(reRunAutograder, "spanx 2, growx"); } private void createBacklogPanel() { - backlogPanel = new JBPanel<>().withBorder(new TitledBorder("Backlog")); - backlogPanel.setLayout(new MigLayout("", "[grow]")); + backlogPanel = new JBPanel<>(new MigLayout("wrap 2", "[grow] []")) + .withBorder(new TitledBorder(new LineBorder(JBColor.border()), "Backlog")); + + backlogList = new JBPanel<>(new MigLayout("wrap 3, gapx 30", "[][][grow]")); + backlogPanel.add(ScrollPaneFactory.createScrollPane(backlogList, true), "spanx 2, growx"); - backlogList = new JBPanel<>(new MigLayout("wrap 3", "[][][grow]")); - backlogPanel.add(ScrollPaneFactory.createScrollPane(backlogList), "growx"); + var refreshButton = new JButton(AllIcons.Actions.Refresh); + refreshButton.addActionListener(a -> { + updateBacklog(); + ToolWindowManager.getInstance(EditorUtil.getActiveProject()).notifyByBalloon("Grading", MessageType.INFO, "Backlog updated"); + }); + backlogPanel.add(refreshButton, "skip 1, alignx right"); } private void updateBacklog() { @@ -217,23 +269,42 @@ private void updateBacklog() { var exercise = PluginState.getInstance().getActiveExercise().get(); try { - exercise.fetchSubmissions(0, true).forEach(submission -> { - backlogList.add(new JBLabel("12. 9. 2024")); - backlogList.add(new JBLabel("90%")); - - JButton reopenButton; - if (submission.isSubmitted()) { - reopenButton = new JButton("Reopen Assessment"); - } else { - reopenButton = new JButton("Continue Assessment"); - } - reopenButton.addActionListener(a -> PluginState.getInstance().reopenAssessment(submission)); - backlogList.add(reopenButton, "growx"); - }); - } catch (ArtemisNetworkException e) { - ArtemisUtils.displayGenericErrorBalloon("Failed to fetch backlog: " + e.getMessage()); + exercise.fetchSubmissions(0, true) + .stream() + .sorted(Comparator.comparing(ProgrammingSubmission::getSubmissionDate)) + .forEach(submission -> { + String dateText = submission.getSubmissionDate().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT)); + backlogList.add(new JBLabel(dateText), "alignx right"); + + 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"); + + JButton reopenButton; + if (submission.isSubmitted()) { + reopenButton = new JButton("Reopen Assessment"); + } else { + reopenButton = new JButton("Continue Assessment"); + reopenButton.setForeground(JBColor.ORANGE); + } + reopenButton.addActionListener(a -> PluginState.getInstance().reopenAssessment(submission)); + backlogList.add(reopenButton, "growx"); + }); + } catch (ArtemisNetworkException ex) { + LOG.error(ex); + ArtemisUtils.displayNetworkErrorBalloon("Failed to fetch backlog", ex); } updateUI(); } + + private record OptionalExam(Exam exam) { + @Override + public String toString() { + return exam == null ? "" : exam.toString(); + } + } } 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 c53f1e7..0000000 --- a/src/main/java/edu/kit/kastel/extensions/guis/StatisticsContainer.java +++ /dev/null @@ -1,28 +0,0 @@ -/* Licensed under EPL-2.0 2024. */ -package edu.kit.kastel.extensions.guis; - -import com.intellij.ui.components.JBLabel; -import edu.kit.kastel.sdq.artemis4j.grading.Exercise; - -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/tool_windows/AnnotationsToolWindowFactory.java b/src/main/java/edu/kit/kastel/extensions/tool_windows/AnnotationsToolWindowFactory.java index e78876f..140d918 100644 --- a/src/main/java/edu/kit/kastel/extensions/tool_windows/AnnotationsToolWindowFactory.java +++ b/src/main/java/edu/kit/kastel/extensions/tool_windows/AnnotationsToolWindowFactory.java @@ -1,17 +1,11 @@ /* 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.state.PluginState; +import edu.kit.kastel.extensions.guis.AnnotationsListPanel; import org.jetbrains.annotations.NotNull; /** @@ -19,24 +13,9 @@ */ 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 (!PluginState.getInstance().isConnected()) { - return; - } - - // add content to menu panel - contentPanel.add(generatedMenu); - Content content = ContentFactory.getInstance().createContent(this.contentPanel, null, false); - + var content = ContentFactory.getInstance().createContent(new AnnotationsListPanel(), null, false); toolWindow.getContentManager().addContent(content); } } 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 index 9ccde8d..87443ea 100644 --- a/src/main/java/edu/kit/kastel/extensions/tool_windows/MainToolWindowFactory.java +++ b/src/main/java/edu/kit/kastel/extensions/tool_windows/MainToolWindowFactory.java @@ -1,41 +1,13 @@ /* Licensed under EPL-2.0 2024. */ package edu.kit.kastel.extensions.tool_windows; -import java.awt.GridLayout; -import java.awt.event.ItemEvent; -import java.util.Comparator; -import java.util.Locale; -import java.util.Objects; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JPanel; - -import com.intellij.DynamicBundle; -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; import com.intellij.openapi.project.DumbAware; 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.JBColor; -import com.intellij.ui.components.JBPanel; -import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactory; -import edu.kit.kastel.extensions.guis.AssessmentViewContent; +import edu.kit.kastel.extensions.guis.AssessmentPanel; import edu.kit.kastel.extensions.guis.ExercisePanel; -import edu.kit.kastel.extensions.settings.ArtemisSettingsState; -import edu.kit.kastel.listeners.GradingConfigSelectedListener; -import edu.kit.kastel.sdq.artemis4j.ArtemisClientException; -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.state.ActiveAssessment; -import edu.kit.kastel.state.PluginState; -import edu.kit.kastel.utils.ArtemisUtils; import org.jetbrains.annotations.NotNull; /** @@ -43,176 +15,9 @@ * It does not handle any other logic, that should be factored out. */ public class MainToolWindowFactory implements ToolWindowFactory, DumbAware { - - 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."; - private static final Locale LOCALE = DynamicBundle.getLocale(); - - // 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 ComboBox coursesComboBox = generatedMenu.getCoursesDropdown(); - private final ComboBox examsComboBox = generatedMenu.getExamsDropdown(); - private final ComboBox exerciseComboBox = generatedMenu.getExercisesDropdown(); - private final ComboBox backlogComboBox = generatedMenu.getBacklogSelector(); - - 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 (!PluginState.getInstance().isConnected()) { - 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); - } - - toolWindow.getContentManager().addContent(content); toolWindow.getContentManager().addContent(ContentFactory.getInstance().createContent(new ExercisePanel(), "Exercise", false)); - - PluginState.getInstance().registerAssessmentStartedListener(this::onAssessmentStarted); - } - - private void addListeners() { - gradingConfigInput.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()); - - coursesComboBox.addItemListener(itemEvent -> { - if (itemEvent.getStateChange() == ItemEvent.DESELECTED) { - return; - } - - Course selectedCourse = ((Course) itemEvent.getItem()); - PluginState.getInstance().setActiveCourse(selectedCourse); - populateExamDropdown(selectedCourse); - populateExercisesDropdown(selectedCourse); - }); - - exerciseComboBox.addItemListener(e -> { - if (e.getStateChange() != ItemEvent.DESELECTED) { - PluginState.getInstance().setActiveExercise((ProgrammingExercise) e.getItem()); - } - }); - - // add listener for Button that starts first grading round - startAssessment1Btn.addActionListener(e -> PluginState.getInstance().startNextAssessment(0)); - - // button that saves assessment - saveAssessmentBtn.addActionListener(e -> { - // TODO - }); - - // button that submits assessment - submitAssessmentBtn.addActionListener(e -> PluginState.getInstance().submitAssessment()); - } - - private void populateDropdowns() throws ArtemisClientException { - var connection = PluginState.getInstance().getConnection().orElseThrow(); - - // add all courses to the courses dropdown - coursesComboBox.removeAllItems(); - connection.getCourses().forEach(coursesComboBox::addItem); - - // populate the dropdowns once because on load event listener is not triggered - Course initial = ((Course) Objects.requireNonNull(coursesComboBox.getSelectedItem())); - 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(null); - - try { - course.getExams().forEach(examsComboBox::addItem); - } catch (ArtemisClientException e) { - ArtemisUtils.displayGenericErrorBalloon(String.format(EXAMS_FETCH_ERROR_FORMAT, course)); - } - } - - private void populateExercisesDropdown(@NotNull Course course) { - exerciseComboBox.removeAllItems(); - - try { - course.getProgrammingExercises().forEach(exerciseComboBox::addItem); - } catch (ArtemisClientException e) { - ArtemisUtils.displayGenericErrorBalloon(String.format(EXERCISES_FETCH_ERROR_FORMAT, course)); - } - } - - private void onAssessmentStarted(ActiveAssessment assessment) { - // clear content before adding new - generatedMenu.getRatingGroupContainer().removeAll(); - - // add all rating groups - assessment.getGradingConfig().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().translateTo(LOCALE), - ratingGroup.getMinPenalty(), - ratingGroup.getMaxPenalty()))); - - // add buttons to rating group - ratingGroup.getMistakeTypes().stream() - // sort buttons alphabetically - // TODO: for some reason this is broken - .sorted(Comparator.comparing( - mistake -> mistake.getButtonText().translateTo(LOCALE))) - .forEach(mistakeType -> { - // create button, add listener and add it to the container - JButton assessmentButton = - new JButton(mistakeType.getButtonText().translateTo(LOCALE)); - assessmentButton.addActionListener(e -> { - // add annotation to the current caret position - PluginState.getInstance() - .getActiveAssessment() - .orElseThrow() - .addAnnotationAtCaret(mistakeType, false); - }); - ratingCroupContainer.add(assessmentButton); - }); - - generatedMenu.getRatingGroupContainer().add(ratingCroupContainer); - }); + toolWindow.getContentManager().addContent(ContentFactory.getInstance().createContent(new AssessmentPanel(), "Grading", false)); } } diff --git a/src/main/java/edu/kit/kastel/highlighter/HighlighterManager.java b/src/main/java/edu/kit/kastel/highlighter/HighlighterManager.java index efbab1b..dbfe701 100644 --- a/src/main/java/edu/kit/kastel/highlighter/HighlighterManager.java +++ b/src/main/java/edu/kit/kastel/highlighter/HighlighterManager.java @@ -1,7 +1,11 @@ package edu.kit.kastel.highlighter; import com.intellij.DynamicBundle; +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; 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.HighlighterLayer; import com.intellij.openapi.editor.markup.HighlighterTargetArea; @@ -11,9 +15,12 @@ import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.FileEditorManagerListener; import com.intellij.openapi.fileEditor.TextEditor; -import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.ui.popup.JBPopup; +import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.JBColor; +import com.intellij.ui.awt.RelativePoint; +import com.intellij.ui.components.JBLabel; import edu.kit.kastel.extensions.settings.ArtemisSettingsState; import edu.kit.kastel.sdq.artemis4j.grading.Annotation; import edu.kit.kastel.state.PluginState; @@ -28,10 +35,15 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class HighlighterManager { - private static final Map> highlightersPerEditor = new IdentityHashMap<>(); - private static final Map> highlightersPerAnnotation = new HashMap<>(); + private static final Map> highlightersPerEditor = new IdentityHashMap<>(); + private static final Map> highlightersPerAnnotation = new HashMap<>(); + + private static int lastPopupLine; + private static Editor lastPopupEditor; + private static JBPopup lastPopup; public static void initialize() { var messageBus = EditorUtil.getActiveProject().getMessageBus(); @@ -41,12 +53,13 @@ public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile f FileEditorManagerListener.super.fileOpened(source, file); var editor = source.getSelectedTextEditor(); - var filePath = EditorUtil.getProjectRootDirectory().relativize(Path.of(file.getPath())).toString(); + editor.getDocument().setReadOnly(true); + var filePath = Path.of(file.getPath()); var state = PluginState.getInstance(); if (state.isAssessing()) { for (var annotation : state.getActiveAssessment().get().getAssessment().getAnnotations()) { - if (annotation.getFilePath().equals(filePath)) { + if (EditorUtil.getAnnotationPath(annotation).equals(filePath)) { createHighlighter(editor, annotation); } } @@ -61,40 +74,107 @@ public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile f var highlighters = highlightersPerEditor.get(editor); if (highlighters != null) { for (var highlighter : highlighters) { - for (var entry: highlightersPerAnnotation.entrySet()) { - entry.getValue().remove(highlighter); + var highlightersForAnnotation = highlightersPerAnnotation.get(highlighter.annotation()); + if (highlightersForAnnotation != null) { + highlightersForAnnotation.removeIf(h -> h.highlighter().equals(highlighter.highlighter())); } } } } }); + + // When an assessment is closed, clear everything + PluginState.getInstance().registerAssessmentClosedListener(() -> { + highlightersPerAnnotation.clear(); + highlightersPerEditor.clear(); + cancelLastPopup(); + }); } public static void createHighlighter(Annotation annotation) { - var file = getAnnotationFile(annotation); + var file = EditorUtil.getAnnotationFile(annotation); // The file may be open in multiple editors FileEditorManager.getInstance(EditorUtil.getActiveProject()) .getAllEditorList(file) .forEach(editor -> createHighlighter(((TextEditor) editor).getEditor(), annotation)); + + // Invalidate all popups, since their content may need to change + cancelLastPopup(); } public static void deleteHighlighter(Annotation annotation) { var highlighters = highlightersPerAnnotation.get(annotation); if (highlighters != null) { + // Delete the annotation -> highlighter mapping + highlightersPerAnnotation.remove(annotation); + + // Also delete each highlighter from its editor for (var highlighter : highlighters) { - for (var entry : highlightersPerEditor.entrySet()) { - if (entry.getValue().contains(highlighter)) { - entry.getValue().remove(highlighter); - entry.getKey().getMarkupModel().removeHighlighter(highlighter); - } + var highlightersForEditor = highlightersPerEditor.get(highlighter.editor()); + if (highlightersForEditor != null) { + highlightersForEditor.removeIf(h -> { + if (h.highlighter().equals(highlighter.highlighter())) { + highlighter.editor().getMarkupModel().removeHighlighter(highlighter.highlighter()); + return true; + } else { + return false; + } + }); + } } } + + // Invalidate all popups, since their content may need to change + cancelLastPopup(); + } + + public static void onMouseMovedInEditor(EditorMouseEvent e) { + 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, Annotation annotation) { - var document = FileDocumentManager.getInstance().getDocument(getAnnotationFile(annotation)); + var document = FileDocumentManager.getInstance().getDocument(EditorUtil.getAnnotationFile(annotation)); int startOffset = document.getLineStartOffset(annotation.getStartLine()); int endOffset = document.getLineEndOffset(annotation.getEndLine()); @@ -117,18 +197,26 @@ private static void createHighlighter(Editor editor, Annotation annotation) { annotation.getMistakeType().getButtonText().translateTo(DynamicBundle.getLocale())); highlightersPerEditor.computeIfAbsent(editor, e -> new ArrayList<>()); - highlightersPerEditor.get(editor).add(highlighter); + highlightersPerEditor.get(editor).add(new HighlighterWithAnnotation(highlighter, annotation)); highlightersPerAnnotation.computeIfAbsent(annotation, a -> new ArrayList<>()); - highlightersPerAnnotation.get(annotation).add(highlighter); + highlightersPerAnnotation.get(annotation).add(new HighlighterWithEditor(highlighter, editor)); } - private static VirtualFile getAnnotationFile(Annotation annotation) { - var path = EditorUtil.getProjectRootDirectory().resolve(annotation.getFilePath()); - var file = VfsUtil.findFile(path, true); - if (file == null) { - throw new IllegalStateException("File not found: " + path); + private static void cancelLastPopup() { + if (lastPopup != null) { + if (!lastPopup.isDisposed()) { + lastPopup.cancel(); + } + lastPopup = null; + lastPopupLine = -1; + lastPopupEditor = null; } - return file; + } + + private record HighlighterWithAnnotation(RangeHighlighter highlighter, Annotation annotation) { + } + + private record HighlighterWithEditor(RangeHighlighter highlighter, Editor editor) { } } diff --git a/src/main/java/edu/kit/kastel/highlighter/WrapperPanel.java b/src/main/java/edu/kit/kastel/highlighter/WrapperPanel.java new file mode 100644 index 0000000..ebea0f1 --- /dev/null +++ b/src/main/java/edu/kit/kastel/highlighter/WrapperPanel.java @@ -0,0 +1,34 @@ +package edu.kit.kastel.highlighter; + +import com.intellij.ui.WidthBasedLayout; + +import javax.swing.*; +import java.awt.*; + +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); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/kastel/listeners/OnMouseInEditorMoved.java b/src/main/java/edu/kit/kastel/listeners/OnMouseInEditorMoved.java new file mode 100644 index 0000000..b32dea9 --- /dev/null +++ b/src/main/java/edu/kit/kastel/listeners/OnMouseInEditorMoved.java @@ -0,0 +1,13 @@ +package edu.kit.kastel.listeners; + +import com.intellij.openapi.editor.event.EditorMouseEvent; +import com.intellij.openapi.editor.event.EditorMouseMotionListener; +import edu.kit.kastel.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/state/ActiveAssessment.java b/src/main/java/edu/kit/kastel/state/ActiveAssessment.java index 56ad5ae..d61f18f 100644 --- a/src/main/java/edu/kit/kastel/state/ActiveAssessment.java +++ b/src/main/java/edu/kit/kastel/state/ActiveAssessment.java @@ -27,11 +27,14 @@ import java.awt.EventQueue; import java.awt.event.InputEvent; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; public class ActiveAssessment { + public static final Path ASSIGNMENT_SUB_PATH = Path.of("assignment"); + private final List>> annotationsUpdatedListener = new ArrayList<>(); private final Assessment assessment; @@ -63,15 +66,17 @@ public void addAnnotationAtCaret(MistakeType mistakeType, boolean withCustomMess var selection = CodeSelection.fromCaret(); if (selection.isEmpty()) { - ArtemisUtils.displayGenericErrorBalloon("No code selected"); + ArtemisUtils.displayGenericErrorBalloon("Could not create annotation", "No code selected"); return; } - var selectedText = selection.get().text(); var editor = EditorUtil.getActiveEditor(); - int startLine = editor.getDocument().getLineNumber(selectedText.getStartOffset()); - int endLine = editor.getDocument().getLineNumber(selectedText.getEndOffset()); - String path = selection.get().projectRelativePath().toString(); + int startLine = editor.getDocument().getLineNumber(selection.get().startOffset()); + int endLine = editor.getDocument().getLineNumber(selection.get().endOffset()); + String path = Path.of(EditorUtil.getActiveProject().getBasePath()) + .resolve(ASSIGNMENT_SUB_PATH) + .relativize(selection.get().path()) + .toString(); if (mistakeType.isCustomAnnotation()) { addCustomAnnotation(mistakeType, startLine, endLine, path); @@ -124,10 +129,8 @@ private void addCustomAnnotation(MistakeType mistakeType, int startLine, int end var customScore = new JSpinner(new SpinnerNumberModel(0.0, minValue, maxValue, 0.5)); panel.add(customScore, "spanx 2, growx"); - panel.add(new JSeparator(), "growx"); - var okButton = new JButton("Create"); - panel.add(okButton, "tag ok"); + panel.add(okButton, "skip 1, tag ok"); var popup = JBPopupFactory.getInstance() .createComponentPopupBuilder(panel, customMessage) diff --git a/src/main/java/edu/kit/kastel/state/PluginState.java b/src/main/java/edu/kit/kastel/state/PluginState.java index 8821e08..ad8b915 100644 --- a/src/main/java/edu/kit/kastel/state/PluginState.java +++ b/src/main/java/edu/kit/kastel/state/PluginState.java @@ -1,46 +1,50 @@ /* Licensed under EPL-2.0 2024. */ package edu.kit.kastel.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.ExecutionException; -import java.util.function.Consumer; - 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.fileEditor.FileEditorManager; import com.intellij.openapi.ui.MessageDialogBuilder; -import com.intellij.openapi.vfs.newvfs.RefreshQueue; import com.intellij.ui.jcef.JBCefApp; import com.intellij.ui.jcef.JBCefCookie; import edu.kit.kastel.extensions.settings.ArtemisSettingsState; import edu.kit.kastel.login.CefUtils; 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.Course; import edu.kit.kastel.sdq.artemis4j.grading.Exam; +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.utils.ArtemisUtils; import edu.kit.kastel.utils.EditorUtil; +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.ExecutionException; +import java.util.function.Consumer; + public class PluginState { + private static final Logger LOG = Logger.getInstance(PluginState.class); + 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 Logger log = Logger.getInstance(PluginState.class); private static PluginState pluginState; private final List connectedListeners = new ArrayList<>(); @@ -84,8 +88,9 @@ public boolean connect() { this.connectedListeners.forEach(Runnable::run); return true; - } catch (ArtemisClientException ex) { - ArtemisUtils.displayGenericErrorBalloon("Error connecting to Artemis: %s".formatted(ex.getMessage())); + } catch (ArtemisClientException e) { + LOG.error(e); + ArtemisUtils.displayGenericErrorBalloon("Artemis login failed", e.getMessage()); return false; } } @@ -106,16 +111,13 @@ public boolean isAssessing() { } public void startNextAssessment(int correctionRound) { - if (activeAssessment != null - && !MessageDialogBuilder.yesNo("Unsaved assessment", LOOSE_ASSESSMENT_MSG) - .guessWindowAndAsk()) { - ArtemisUtils.displayGenericErrorBalloon("Please finish the current assessment first"); + if (activeAssessment != null) { + ArtemisUtils.displayFinishAssessmentFirstBalloon(); return; } if (activeCourse == null || activeExercise == null) { - ArtemisUtils.displayGenericErrorBalloon( - "Please select a course and exercise: " + activeCourse + " " + activeExercise); + ArtemisUtils.displayGenericErrorBalloon("Could not start assessment", "No course selected"); return; } @@ -129,64 +131,84 @@ public void startNextAssessment(int correctionRound) { if (nextAssessment.isPresent()) { this.initializeAssessment(nextAssessment.get()); } else { - ArtemisUtils.displayGenericErrorBalloon( - "There are no more submissions to assess. Thanks for your work :)"); + ArtemisUtils.displayGenericInfoBalloon("Could not start assessment", "There are no more submissions to assess. Thanks for your work :)"); } - } catch (ArtemisClientException e) { - ArtemisUtils.displayGenericErrorBalloon("Error starting assessment: %s".formatted(e.getMessage())); + } catch (ArtemisNetworkException e) { + LOG.error(e); + ArtemisUtils.displayNetworkErrorBalloon("Could not lock assessment", e); + } catch (AnnotationMappingException e) { + LOG.error(e); + ArtemisUtils.displayGenericErrorBalloon("Could not parse assessment", "Could not parse previous assessment. This is a serious bug; please contact the Übungsleitung!"); } } public void saveAssessment() { if (activeAssessment == null) { - ArtemisUtils.displayGenericErrorBalloon("No active assessment"); + ArtemisUtils.displayNoAssessmentBalloon(); return; } try { activeAssessment.getAssessment().save(); - } catch (ArtemisClientException e) { - ArtemisUtils.displayGenericErrorBalloon("Error saving assessment: %s".formatted(e.getMessage())); + } catch (ArtemisNetworkException e) { + LOG.error(e); + ArtemisUtils.displayNetworkErrorBalloon("Could not save assessment", e); + } catch (AnnotationMappingException e) { + LOG.error(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.displayGenericErrorBalloon("No active assessment"); + ArtemisUtils.displayNoAssessmentBalloon(); return; } try { activeAssessment.getAssessment().submit(); this.cleanupAssessment(); - } catch (ArtemisClientException e) { - ArtemisUtils.displayGenericErrorBalloon("Error submitting assessment: %s".formatted(e.getMessage())); + } catch (ArtemisNetworkException e) { + LOG.error(e); + ArtemisUtils.displayNetworkErrorBalloon("Could not submit assessment", e); + } catch (AnnotationMappingException e) { + LOG.error(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.displayGenericErrorBalloon("No active assessment"); + ArtemisUtils.displayNoAssessmentBalloon(); return; } try { activeAssessment.getAssessment().cancel(); this.cleanupAssessment(); - } catch (ArtemisClientException e) { - ArtemisUtils.displayGenericErrorBalloon("Error cancelling assessment: %s".formatted(e.getMessage())); + } catch (ArtemisNetworkException e) { + LOG.error(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.displayGenericErrorBalloon("Please finish the current assessment first"); + ArtemisUtils.displayFinishAssessmentFirstBalloon(); return; } if (activeCourse == null || activeExercise == null) { - ArtemisUtils.displayGenericErrorBalloon( - "Please select a course and exercise: " + activeCourse + " " + activeExercise); + ArtemisUtils.displayGenericErrorBalloon("Could not reopen assessment", "No course or exercise selected"); return; } @@ -200,10 +222,17 @@ public void reopenAssessment(ProgrammingSubmission submission) { if (assessment.isPresent()) { this.initializeAssessment(assessment.get()); } else { - ArtemisUtils.displayGenericErrorBalloon("Failed to reopen assessment"); + ArtemisUtils.displayGenericErrorBalloon("Failed to reopen assessment", "Most likely, your lock has been taken by someone else."); } - } catch (ArtemisClientException e) { - ArtemisUtils.displayGenericErrorBalloon("Error reopening assessment: %s".formatted(e.getMessage())); + } catch (ArtemisNetworkException e) { + LOG.error(e); + ArtemisUtils.displayNetworkErrorBalloon("Could not lock assessment", e); + } catch (AnnotationMappingException e) { + LOG.error(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.error(e); + ArtemisUtils.displayGenericErrorBalloon("Could not reopen assessment", "The student has submitted a newer version of his code."); } } @@ -279,14 +308,6 @@ private void retrieveNewJWT() throws ArtemisClientException { private void initializeAssessment(Assessment assessment) { try { - // 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()); - // Cleanup first, in case there are files left from a previous assessment this.cleanupProjectDirectory(); @@ -302,15 +323,17 @@ private void initializeAssessment(Assessment assessment) { this.activeAssessment = new ActiveAssessment(assessment, clonedSubmission); this.assessmentStartedListeners.forEach(listener -> listener.accept(activeAssessment)); + } catch (IOException | ArtemisClientException e) { - log.error("Error cloning submission", e); - ArtemisUtils.displayGenericErrorBalloon("Error cloning submission: %s".formatted(e.getMessage())); + LOG.error(e); + ArtemisUtils.displayGenericErrorBalloon("Error cloning submission", e.getMessage()); + // Cancel the assessment to prevent spurious locks try { assessment.cancel(); - } catch (ArtemisClientException ex) { - ArtemisUtils.displayGenericErrorBalloon( - "Failed to free the assessment lock: %s".formatted(ex.getMessage())); + } catch (ArtemisNetworkException ex) { + LOG.error(ex); + ArtemisUtils.displayGenericErrorBalloon("Failed to free the assessment lock", ex.getMessage()); } } } @@ -335,7 +358,7 @@ private void cleanupAssessment() { private Optional createGradingConfig() { var gradingConfigPath = ArtemisSettingsState.getInstance().getSelectedGradingConfigPath(); if (gradingConfigPath == null) { - ArtemisUtils.displayGenericErrorBalloon("Please select a grading config"); + ArtemisUtils.displayGenericErrorBalloon("No grading config", "Please select a grading config"); return Optional.empty(); } @@ -343,12 +366,19 @@ private Optional createGradingConfig() { return Optional.of( GradingConfig.readFromString(Files.readString(Path.of(gradingConfigPath)), activeExercise)); } catch (IOException | InvalidGradingConfigException e) { - ArtemisUtils.displayGenericErrorBalloon("Invalid grading config: %s".formatted(e.getMessage())); + LOG.error(e); + ArtemisUtils.displayGenericErrorBalloon("Invalid grading config", e.getMessage()); return Optional.empty(); } } private void cleanupProjectDirectory() { + // Close all open editors + var editorManager = FileEditorManager.getInstance(EditorUtil.getActiveProject()); + for (var editor : editorManager.getAllEditors()) { + editorManager.closeFile(editor.getFile()); + } + var rootPath = EditorUtil.getProjectRootDirectory(); try { Files.walkFileTree(rootPath, new SimpleFileVisitor<>() { @@ -367,9 +397,8 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } }); } catch (IOException e) { - log.error("Error cleaning up project directory", e); - ArtemisUtils.displayGenericErrorBalloon( - "Error cleaning up project directory: %s".formatted(e.getMessage())); + LOG.error(e); + ArtemisUtils.displayGenericErrorBalloon("Error cleaning up project directory", e.getMessage()); } } } diff --git a/src/main/java/edu/kit/kastel/utils/ArtemisUtils.java b/src/main/java/edu/kit/kastel/utils/ArtemisUtils.java index 9c62d65..9f217d1 100644 --- a/src/main/java/edu/kit/kastel/utils/ArtemisUtils.java +++ b/src/main/java/edu/kit/kastel/utils/ArtemisUtils.java @@ -5,6 +5,7 @@ import com.intellij.notification.NotificationGroupManager; import com.intellij.notification.NotificationType; import com.intellij.openapi.actionSystem.AnAction; +import edu.kit.kastel.sdq.artemis4j.ArtemisNetworkException; import edu.kit.kastel.sdq.artemis4j.grading.ArtemisConnection; import edu.kit.kastel.sdq.artemis4j.grading.Assessment; import edu.kit.kastel.sdq.artemis4j.grading.Course; @@ -27,45 +28,8 @@ public final class ArtemisUtils { private static Assessment activeAssessment; 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. * @@ -87,16 +51,43 @@ public static void displayLoginErrorBalloon(String msg, @Nullable AnAction 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) { + 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(balloonContent, NotificationType.ERROR) - .setTitle(ArtemisUtils.GENERIC_ARTEMIS_ERROR_TITLE) + .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 displayGenericExceptionBalloon(Exception e) { + displayGenericErrorBalloon("IntelliGrade Error", e.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/utils/CodeSelection.java b/src/main/java/edu/kit/kastel/utils/CodeSelection.java index eaa72e7..8eb2bf1 100644 --- a/src/main/java/edu/kit/kastel/utils/CodeSelection.java +++ b/src/main/java/edu/kit/kastel/utils/CodeSelection.java @@ -8,17 +8,26 @@ import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; -public record CodeSelection(TextRange text, PsiElement element) { +public record CodeSelection(int startOffset, int endOffset, PsiElement element) { public static Optional fromCaret() { var editor = EditorUtil.getActiveEditor(); - if (editor == null || !editor.getSelectionModel().hasSelection()) { + if (editor == null) { // no editor open or no selection made return Optional.empty(); } - // get editor Selection - TextRange selectedText = editor.getCaretModel().getPrimaryCaret().getSelectionRange(); + 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(); + } // only annotate if a selection has been made // get the currently selected element and the containing file @@ -27,14 +36,10 @@ public static Optional fromCaret() { .findElementAt(editor.getCaretModel().getOffset()) .getContext(); - return Optional.of(new CodeSelection(selectedText, selectedElement)); + return Optional.of(new CodeSelection(startOffset, endOffset, selectedElement)); } public Path path() { return Path.of(element.getContainingFile().getVirtualFile().getPath()); } - - public Path projectRelativePath() { - return Path.of(element.getProject().getBasePath()).relativize(path()); - } } diff --git a/src/main/java/edu/kit/kastel/utils/EditorUtil.java b/src/main/java/edu/kit/kastel/utils/EditorUtil.java index 60f20db..6ee8712 100644 --- a/src/main/java/edu/kit/kastel/utils/EditorUtil.java +++ b/src/main/java/edu/kit/kastel/utils/EditorUtil.java @@ -10,7 +10,10 @@ 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.state.ActiveAssessment; import org.jetbrains.idea.maven.project.MavenProjectsManager; public class EditorUtil { @@ -41,7 +44,18 @@ public static ProjectLevelVcsManagerImpl getVcsManager() { return ProjectLevelVcsManagerImpl.getInstanceImpl(EditorUtil.getActiveProject()); } - public static int convertPositionToLine(int position) { - return getActiveEditor().getDocument().getLineNumber(position); + public static Path getAnnotationPath(Annotation annotation) { + return EditorUtil.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/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c816d8b..05287bf 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -29,6 +29,7 @@ +