Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger customization notifications on ide, stacking multiple notifications vertically and refactor/add support for more methods in PluginStore #48

Merged
merged 13 commits into from
Oct 4, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@

package software.aws.toolkits.eclipse.amazonq.configuration;

import java.nio.charset.StandardCharsets;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import com.google.gson.Gson;

import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;

public final class PluginStore {
private static final Preferences PREFERENCES = Preferences.userRoot().node("software.aws.toolkits.eclipse");
private static final Gson GSON = new Gson();
private PluginStore() {
// Prevent instantiation
}
Expand All @@ -31,4 +34,23 @@ public static void remove(final String key) {
PREFERENCES.remove(key);
}

public static <T> void putObject(final String key, final T value) {
String jsonValue = GSON.toJson(value);
byte[] byteValue = jsonValue.getBytes(StandardCharsets.UTF_8);
PREFERENCES.putByteArray(key, byteValue);
Comment on lines +38 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this technically can be done serializing entire objects does present additional risks. In general I would like to be convinced that use cases require it, vs. binding relational configuration under a common key namespace, e.g.:

notifications.foo: 123
notifications.bar: 456

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the same reason which I described here https://github.com/aws/amazon-q-eclipse/pull/48/files#r1787324480

Also, currently when LSP server requests for aws.q, we only return customization object but maybe in near future - we might be returning more than just one single row item. Then in those kindof cases, it would be better to just do a single lookup and return it.

try {
PREFERENCES.flush();
} catch (BackingStoreException e) {
PluginLogger.warn(String.format("Error while saving entry to a preference store - key: %s, value: %s", key, value), e);
}
}

public static <T> T getObject(final String key, final Class<T> type) {
byte[] byteValue = PREFERENCES.getByteArray(key, null);
if (byteValue == null) {
return null;
}
String jsonValue = new String(byteValue, StandardCharsets.UTF_8);
return GSON.fromJson(jsonValue, type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

import org.eclipse.lsp4e.LanguageClientImpl;
Expand All @@ -18,6 +19,7 @@
import software.aws.toolkits.eclipse.amazonq.lsp.model.ConnectionMetadata;
import software.aws.toolkits.eclipse.amazonq.lsp.model.SsoProfileData;
import software.aws.toolkits.eclipse.amazonq.util.Constants;
import software.aws.toolkits.eclipse.amazonq.views.model.Customization;
import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;
import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils;

Expand All @@ -42,9 +44,9 @@ public final CompletableFuture<List<Object>> configuration(final ConfigurationPa
List<Object> output = new ArrayList<>();
configurationParams.getItems().forEach(item -> {
if (item.getSection().equals(Constants.LSP_CONFIGURATION_KEY)) {
String customizationArn = PluginStore.get(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY);
Customization storedCustomization = PluginStore.getObject(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, Customization.class);
Map<String, String> customization = new HashMap<>();
customization.put(Constants.LSP_CUSTOMIZATION_CONFIGURATION_KEY, customizationArn);
customization.put(Constants.LSP_CUSTOMIZATION_CONFIGURATION_KEY, Objects.nonNull(storedCustomization) ? storedCustomization.getArn() : null);
output.add(customization);
} else {
output.add(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ private Constants() {
public static final String CUSTOMIZATION_STORAGE_INTERNAL_KEY = "aws.q.customization.eclipse";
public static final String LSP_CUSTOMIZATION_CONFIGURATION_KEY = "customization";
public static final String LSP_CONFIGURATION_KEY = "aws.q";
public static final String IDE_CUSTOMIZATION_NOTIFICATION_TITLE = "Amazon Q Customization";
public static final String IDE_CUSTOMIZATION_NOTIFICATION_BODY_TEMPLATE = "Amazon Q inline suggestions are now coming from the %s";
public static final String DEFAULT_Q_FOUNDATION_DISPLAY_NAME = "Amazon Q foundation (Default)";

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup;

Expand All @@ -21,6 +24,11 @@ public final class ToolkitNotification extends AbstractNotificationPopup {
private final String title;
private final String description;
private Image infoIcon;
private static CopyOnWriteArrayList<ToolkitNotification> activeNotifications = new CopyOnWriteArrayList<>();
private static final int MAX_WIDTH = 400;
private static final int MIN_HEIGHT = 100;
private static final int PADDING_EDGE = 5;
private static final int NOTIFICATIONS_GAP = 5;

public ToolkitNotification(final Display display, final String title, final String description) {
super(display);
Expand Down Expand Up @@ -56,12 +64,50 @@ protected String getPopupShellTitle() {
}

@Override
protected Point getInitialSize() {
return new Point(60, 20);
protected void initializeBounds() {
Rectangle clArea = getPrimaryClientArea();
Point initialSize = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT);
int height = Math.max(initialSize.y, MIN_HEIGHT);
int width = Math.min(initialSize.x, MAX_WIDTH);
Point size = new Point(width, height);
// Calculate the position for the new notification
int x = clArea.x + clArea.width - size.x - PADDING_EDGE;
int y = clArea.height + clArea.y - size.y - PADDING_EDGE;
for (ToolkitNotification notification : activeNotifications) {
if (!notification.getShell().isDisposed()) {
y -= notification.getShell().getSize().y + NOTIFICATIONS_GAP;
}
}
getShell().setLocation(x, y);
getShell().setSize(size);
activeNotifications.add(this);
}

private Rectangle getPrimaryClientArea() {
Monitor primaryMonitor = getShell().getDisplay().getPrimaryMonitor();
return primaryMonitor != null ? primaryMonitor.getClientArea() : getShell().getDisplay().getClientArea();
}

private void repositionNotifications() {
Rectangle clArea = getPrimaryClientArea();
Point initialSize = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT);
int height = Math.max(initialSize.y, MIN_HEIGHT);
int width = Math.min(initialSize.x, MAX_WIDTH);
int x = clArea.x + clArea.width - width - PADDING_EDGE;
int y = clArea.height + clArea.y - height - PADDING_EDGE;
for (ToolkitNotification notification : activeNotifications) {
if (!notification.getShell().isDisposed()) {
Point size = notification.getShell().getSize();
notification.getShell().setLocation(x, y);
y -= size.y + NOTIFICATIONS_GAP;
}
}
}

@Override
public boolean close() {
activeNotifications.remove(this);
repositionNotifications();
if (this.infoIcon != null && !this.infoIcon.isDisposed()) {
this.infoIcon.dispose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
Expand All @@ -34,7 +37,9 @@
import software.aws.toolkits.eclipse.amazonq.util.Constants;
import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;
import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils;
import software.aws.toolkits.eclipse.amazonq.util.ToolkitNotification;
import software.aws.toolkits.eclipse.amazonq.views.model.Customization;
import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup;

public final class CustomizationDialog extends Dialog {

Expand All @@ -45,7 +50,7 @@ public final class CustomizationDialog extends Dialog {
private Font boldFont;
private List<Customization> customizationsResponse;
private ResponseSelection responseSelection;
private String selectedCustomisationArn;
private Customization selectedCustomization;

public enum ResponseSelection {
AMAZON_Q_FOUNDATION_DEFAULT,
Expand Down Expand Up @@ -80,6 +85,10 @@ private final class CustomRadioButton extends Composite {
public Button getRadioButton() {
return radioButton;
}

public Label getTextLabel() {
return textLabel;
}
}

public CustomizationDialog(final Shell parentShell) {
Expand All @@ -102,12 +111,19 @@ public ResponseSelection getResponseSelection() {
return this.responseSelection;
}

public void setSelectedCustomizationArn(final String arn) {
this.selectedCustomisationArn = arn;
public void setSelectedCustomization(final Customization customization) {
this.selectedCustomization = customization;
}

public Customization getSelectedCustomization() {
return this.selectedCustomization;
}

public String getSelectedCustomizationArn() {
return this.selectedCustomisationArn;
private void showNotification(final String customizationName) {
AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(),
Constants.IDE_CUSTOMIZATION_NOTIFICATION_TITLE,
String.format(Constants.IDE_CUSTOMIZATION_NOTIFICATION_BODY_TEMPLATE, customizationName));
notification.open();
}

@Override
Expand All @@ -118,16 +134,17 @@ protected void createButtonsForButtonBar(final Composite parent) {

@Override
protected void okPressed() {
PluginLogger.info(String.format("Select pressed with responseSelection:%s and selectedArn:%s", this.responseSelection, this.selectedCustomisationArn));
if (this.responseSelection.equals(ResponseSelection.AMAZON_Q_FOUNDATION_DEFAULT)) {
PluginStore.remove(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY);
} else if (this.selectedCustomisationArn != null) {
PluginStore.put(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, this.selectedCustomisationArn);
Display.getCurrent().asyncExec(() -> showNotification(Constants.DEFAULT_Q_FOUNDATION_DISPLAY_NAME));
} else if (Objects.nonNull(this.getSelectedCustomization()) && StringUtils.isNotBlank(this.getSelectedCustomization().getName())) {
PluginStore.putObject(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, this.getSelectedCustomization());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not just store the ARN?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier I was just storing ARN only but I need to have name (or maybe description as well later) for showing notifications. So instead of creating 3 different getter/setter for all of these, I changed the implementation to just have a single setter/getter.

Map<String, Object> updatedSettings = new HashMap<>();
Map<String, String> internalMap = new HashMap<>();
internalMap.put(Constants.LSP_CUSTOMIZATION_CONFIGURATION_KEY, this.selectedCustomisationArn);
internalMap.put(Constants.LSP_CUSTOMIZATION_CONFIGURATION_KEY, this.getSelectedCustomization().getArn());
updatedSettings.put(Constants.LSP_CONFIGURATION_KEY, internalMap);
ThreadingUtils.executeAsyncTask(() -> CustomizationUtil.triggerChangeConfigurationNotification(updatedSettings));
Display.getCurrent().asyncExec(() -> showNotification(String.format("%s customization", this.getSelectedCustomization().getName())));
}
super.okPressed();
}
Expand Down Expand Up @@ -179,10 +196,10 @@ private void updateComboOnUIThread(final List<Customization> customizations) {
int defaultSelectedDropdownIndex = -1;
for (int index = 0; index < customizations.size(); index++) {
addFormattedOption(combo, customizations.get(index).getName(), customizations.get(index).getDescription());
combo.setData(String.format("%s", index), customizations.get(index).getArn());
combo.setData(String.format("%s", index), customizations.get(index));
if (this.responseSelection.equals(ResponseSelection.CUSTOMIZATION)
&& StringUtils.isNotBlank(this.selectedCustomisationArn)
&& this.selectedCustomisationArn.equals(customizations.get(index).getArn())) {
&& Objects.nonNull(this.getSelectedCustomization())
&& this.getSelectedCustomization().getArn().equals(customizations.get(index).getArn())) {
defaultSelectedDropdownIndex = index;
}
}
Expand All @@ -197,9 +214,9 @@ private void updateComboOnUIThread(final List<Customization> customizations) {
public void widgetSelected(final SelectionEvent e) {
int selectedIndex = combo.getSelectionIndex();
String selectedOption = combo.getItem(selectedIndex);
String selectedCustomizationArn = (String) combo.getData(String.valueOf(selectedIndex));
CustomizationDialog.this.selectedCustomisationArn = selectedCustomizationArn;
PluginLogger.info(String.format("Selected option:%s with arn:%s", selectedOption, selectedCustomizationArn));
Customization selectedCustomization = (Customization) combo.getData(String.valueOf(selectedIndex));
CustomizationDialog.this.setSelectedCustomization(selectedCustomization);
PluginLogger.info(String.format("Selected option:%s with arn:%s", selectedOption, selectedCustomization.getArn()));
}
});
}
Expand Down Expand Up @@ -254,7 +271,17 @@ protected Control createDialogArea(final Composite parent) {
public void widgetSelected(final SelectionEvent e) {
customizationButton.getRadioButton().setSelection(false);
responseSelection = ResponseSelection.AMAZON_Q_FOUNDATION_DEFAULT;
selectedCustomisationArn = null;
setSelectedCustomization(null);
combo.setEnabled(false);
}
});
defaultAmazonQFoundationButton.getTextLabel().addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(final MouseEvent e) {
customizationButton.getRadioButton().setSelection(false);
defaultAmazonQFoundationButton.getRadioButton().setSelection(true);
responseSelection = ResponseSelection.AMAZON_Q_FOUNDATION_DEFAULT;
setSelectedCustomization(null);
combo.setEnabled(false);
}
});
Expand All @@ -266,6 +293,15 @@ public void widgetSelected(final SelectionEvent e) {
combo.setEnabled(true);
}
});
customizationButton.getTextLabel().addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(final MouseEvent e) {
defaultAmazonQFoundationButton.getRadioButton().setSelection(false);
customizationButton.getRadioButton().setSelection(true);
responseSelection = ResponseSelection.CUSTOMIZATION;
combo.setEnabled(true);
}
});
createDropdownForCustomizations(container);
createSeparator(container);
return container;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package software.aws.toolkits.eclipse.amazonq.views.actions;

import java.util.Objects;
import org.eclipse.jface.action.ContributionItem;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
Expand All @@ -13,11 +14,11 @@
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IViewSite;
import jakarta.inject.Inject;
import software.amazon.awssdk.utils.StringUtils;
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore;
import software.aws.toolkits.eclipse.amazonq.util.AuthStatusChangedListener;
import software.aws.toolkits.eclipse.amazonq.util.Constants;
import software.aws.toolkits.eclipse.amazonq.views.CustomizationDialog;
import software.aws.toolkits.eclipse.amazonq.views.model.Customization;
import software.aws.toolkits.eclipse.amazonq.views.CustomizationDialog.ResponseSelection;

public final class CustomizationDialogContributionItem extends ContributionItem implements AuthStatusChangedListener {
Expand Down Expand Up @@ -53,13 +54,13 @@ public void fill(final Menu menu, final int index) {
@Override
public void widgetSelected(final SelectionEvent e) {
CustomizationDialog dialog = new CustomizationDialog(shell);
String storedCustomizationArn = PluginStore.get(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY);
if (StringUtils.isBlank(storedCustomizationArn)) {
Customization storedCustomization = PluginStore.getObject(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, Customization.class);
if (Objects.isNull(storedCustomization)) {
dialog.setResponseSelection(ResponseSelection.AMAZON_Q_FOUNDATION_DEFAULT);
dialog.setSelectedCustomizationArn(null);
dialog.setSelectedCustomization(null);
} else {
dialog.setResponseSelection(ResponseSelection.CUSTOMIZATION);
dialog.setSelectedCustomizationArn(storedCustomizationArn);
dialog.setSelectedCustomization(storedCustomization);
}
dialog.open();
}
Expand Down