package org.geogebra.web.web.gui.util;
import java.util.ArrayList;
import java.util.List;
import org.geogebra.common.gui.util.SelectionTable;
import org.geogebra.common.main.Feature;
import org.geogebra.common.main.Localization;
import org.geogebra.common.main.MaterialsManager;
import org.geogebra.common.move.events.BaseEvent;
import org.geogebra.common.move.ggtapi.events.LogOutEvent;
import org.geogebra.common.move.ggtapi.events.LoginEvent;
import org.geogebra.common.move.ggtapi.models.Chapter;
import org.geogebra.common.move.ggtapi.models.GeoGebraTubeUser;
import org.geogebra.common.move.ggtapi.models.Material;
import org.geogebra.common.move.ggtapi.models.Material.MaterialType;
import org.geogebra.common.move.ggtapi.models.Material.Provider;
import org.geogebra.common.move.views.EventRenderable;
import org.geogebra.common.util.debug.Log;
import org.geogebra.web.html5.euclidian.EuclidianViewWInterface;
import org.geogebra.web.html5.gui.FastClickHandler;
import org.geogebra.web.html5.gui.GPopupPanel;
import org.geogebra.web.html5.gui.textbox.GTextBox;
import org.geogebra.web.html5.gui.tooltip.ToolTipManagerW;
import org.geogebra.web.html5.main.AppW;
import org.geogebra.web.html5.main.StringHandler;
import org.geogebra.web.web.gui.GuiManagerW;
import org.geogebra.web.web.gui.browser.BrowseResources;
import org.geogebra.web.web.gui.dialog.DialogBoxW;
import org.geogebra.web.web.main.FileManager;
import org.geogebra.web.web.move.ggtapi.models.GeoGebraTubeAPIW;
import org.geogebra.web.web.move.ggtapi.models.MaterialCallback;
import org.geogebra.web.web.move.googledrive.operations.GoogleDriveOperationW;
import org.geogebra.web.web.util.SaveCallback;
import org.geogebra.web.web.util.SaveCallback.SaveState;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
/**
* Dialog for online saving (tube/drive)
*/
public class SaveDialogW extends DialogBoxW implements PopupMenuHandler,
EventRenderable {
/** Material visibility */
public enum Visibility {
/** private */
Private(0, "P"),
/** shared with link */
Shared(1, "S"),
/** public */
Public(2, "O");
private int index;
private String token;
Visibility(int index, String tok) {
this.index = index;
this.token = tok;
}
/**
* @return index 0-2
*/
int getIndex() {
return this.index;
}
/**
* @return string representation P/S/O
*/
String getToken() {
return this.token;
}
}
private static final int MAX_TITLE_LENGTH = 60;
/** application */
protected AppW app;
/** title box */
protected TextBox title;
private StandardButton dontSaveButton;
private StandardButton saveButton;
private Label titleLabel;
private final static int MIN_TITLE_LENGTH = 1;
private Runnable runAfterSave;
// SaveCallback saveCallback;
private PopupMenuButtonW providerPopup;
private FlowPanel buttonPanel;
private ListBox listBox;
private MaterialType saveType;
private ArrayList<Material.Provider> supportedProviders = new ArrayList<Material.Provider>();
private Visibility defaultVisibility = Visibility.Private;
private Localization loc;
// private MaterialCallback materialCB;
/**
* @param app
* AppW
*
* Creates a new GeoGebraFileChooser Window <br>
* Use ((DialogManagerW) app.getDialogManager()).getSaveDialog()
* to get a SaveDialog! This looks like a very "special"
* Implementation of a Singleton... ->Refactor?
*/
public SaveDialogW(final AppW app) {
super(app.getPanel());
if (app.has(Feature.DIALOGS_OVERLAP_KEYBOARD)) {
setOverlapFeature(true);
}
this.app = app;
this.loc = app.getLocalization();
this.addStyleName("GeoGebraFileChooser");
this.setGlassEnabled(true);
// this.saveCallback = new SaveCallback(this.app);
FlowPanel contentPanel = new FlowPanel();
this.add(contentPanel);
this.getCaption().setText(loc.getMenu("Save"));
VerticalPanel p = new VerticalPanel();
p.add(getTitelPanel());
p.add(getButtonPanel());
contentPanel.add(p);
this.saveType = MaterialType.ggb;
addCancelButton();
this.addCloseHandler(new CloseHandler<GPopupPanel>() {
@Override
public void onClose(final CloseEvent<GPopupPanel> event) {
handleClose();
}
});
this.addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
app.closePopups();
}
}, ClickEvent.getType());
app.getLoginOperation().getView().add(this);
if (app.getGoogleDriveOperation() != null) {
app.getGoogleDriveOperation().initGoogleDriveApi();
}
}
/**
* Handle dialog closed (escape or cancel)
*/
protected void handleClose() {
app.setDefaultCursor();
dontSaveButton.setEnabled(true);
title.setEnabled(true);
app.closePopupsNoTooltips();
}
/**
* @param base64
* material base64
* @param forked
* whether this is a fork
* @return save callback
*/
MaterialCallback initMaterialCB(final String base64, final boolean forked) {
return new MaterialCallback() {
@Override
public void onLoaded(final List<Material> parseResponse,
ArrayList<Chapter> meta) {
if (isWorksheet()) {
if (parseResponse.size() == 1) {
Material newMat = parseResponse.get(0);
newMat.setThumbnailBase64(((EuclidianViewWInterface) app
.getActiveEuclidianView())
.getCanvasBase64WithTypeString());
app.getKernel().getConstruction()
.setTitle(title.getText());
// last synchronization is equal to last modified
app.setSyncStamp(newMat.getModified());
newMat.setSyncStamp(newMat.getModified());
app.updateMaterialURL(newMat.getId(),
newMat.getSharingKeyOrId());
app.setActiveMaterial(newMat);
app.setSyncStamp(newMat.getModified());
saveLocalIfNeeded(newMat.getModified(),
forked ? SaveState.FORKED : SaveState.OK);
// if we got there via file => new, do the file =>new
// now
runAfterSaveCallback();
} else {
resetCallback();
saveLocalIfNeeded(getCurrentTimestamp(app),
SaveState.ERROR);
}
} else {
if (parseResponse.size() == 1) {
SaveCallback.onSaved(app, SaveState.OK,
isMacro());
} else {
SaveCallback.onSaved(app, SaveState.ERROR,
isMacro());
}
}
hide();
}
@Override
public void onError(final Throwable exception) {
Log.error("SAVE Error" + exception.getMessage());
resetCallback();
((GuiManagerW) app.getGuiManager()).exportGGB();
saveLocalIfNeeded(getCurrentTimestamp(app), SaveState.ERROR);
hide();
}
private void saveLocalIfNeeded(long modified, SaveState state) {
if (isWorksheet()
&& (app.getFileManager().shouldKeep(0)
|| app.has(Feature.LOCALSTORAGE_FILES) || state == SaveState.ERROR)) {
app.getKernel().getConstruction().setTitle(title.getText());
((FileManager) app.getFileManager()).saveFile(base64,
modified, new SaveCallback(app, state));
} else {
SaveCallback.onSaved(app, state, false);
}
}
};
}
/**
* @param app
* used to get current sync stamp
* @return current time in seconds since epoch OR app sync stamp + 1 if
* bigger (to avoid problems with system clock)
*/
public static long getCurrentTimestamp(AppW app) {
return Math.max(System.currentTimeMillis() / 1000,
app.getSyncStamp() + 1);
}
private HorizontalPanel getTitelPanel() {
final HorizontalPanel titlePanel = new HorizontalPanel();
titlePanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
this.titleLabel = new Label(loc.getMenu("Title") + ": ");
titlePanel.add(this.titleLabel);
titlePanel.add(title = new GTextBox());
title.setMaxLength(MAX_TITLE_LENGTH);
title.addKeyUpHandler(new KeyUpHandler() {
@Override
public void onKeyUp(final KeyUpEvent event) {
handleKeyUp(event);
}
});
titlePanel.addStyleName("titlePanel");
return titlePanel;
}
/**
* @param event
* key event
*/
protected void handleKeyUp(KeyUpEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER
&& saveButton.isEnabled()) {
onSave();
} else if (title.getText().length() < MIN_TITLE_LENGTH) {
saveButton.setEnabled(false);
} else {
saveButton.setEnabled(true);
}
}
private FlowPanel getButtonPanel() {
buttonPanel = new FlowPanel();
buttonPanel.addStyleName("buttonPanel");
buttonPanel.add(
dontSaveButton = new StandardButton(loc.getMenu("DontSave")));
buttonPanel.add(saveButton = new StandardButton(loc.getMenu("Save")));
saveButton.addStyleName("saveButton");
dontSaveButton.addStyleName("cancelBtn");
setAvailableProviders();
// ImageOrText[] data, Integer rows, Integer columns, GDimensionW
// iconSize, geogebra.common.gui.util.SelectionTable mode
saveButton.addFastClickHandler(new FastClickHandler() {
@Override
public void onClick(Widget source) {
onSave();
}
});
dontSaveButton.addFastClickHandler(new FastClickHandler() {
@Override
public void onClick(Widget source) {
onDontSave();
}
});
return buttonPanel;
}
private void setAvailableProviders() {
ImageResource[] providerImages = new ImageResource[3];
providerImages[0] = BrowseResources.INSTANCE.location_tube();
int providerCount = 1;
this.supportedProviders.add(Provider.TUBE);
GeoGebraTubeUser user = app.getLoginOperation().getModel()
.getLoggedInUser();
if (user != null && user.hasGoogleDrive()
&& app.getLAF().supportsGoogleDrive()) {
providerImages[providerCount++] = BrowseResources.INSTANCE
.location_drive();
this.supportedProviders.add(Provider.GOOGLE);
}
if (user != null && user.hasOneDrive()) {
providerImages[providerCount++] = BrowseResources.INSTANCE
.location_skydrive();
this.supportedProviders.add(Provider.ONE);
}
if (app.getLAF().supportsLocalSave()) {
providerImages[providerCount++] = BrowseResources.INSTANCE
.location_local();
this.supportedProviders.add(Provider.LOCAL);
}
if (providerPopup != null) {
buttonPanel.remove(providerPopup);
}
providerPopup = new PopupMenuButtonW(app, ImageOrText.convert(
providerImages, 24), 1, providerCount,
SelectionTable.MODE_ICON);
this.providerPopup.getMyPopup().addStyleName("providersPopup");
listBox = new ListBox();
listBox.addStyleName("visibility");
listBox.addItem(loc.getMenu("Private"));
listBox.addItem(loc.getMenu("Shared"));
listBox.addItem(loc.getMenu("Public"));
listBox.setItemSelected(Visibility.Private.getIndex(), true);
if (app.getLAF().supportsGoogleDrive()) {
providerPopup.addPopupHandler(this);
providerPopup.setSelectedIndex(app.getFileManager()
.getFileProvider() == Provider.GOOGLE ? 1 : 0);
}else if(app.getLAF().supportsLocalSave()) {
providerPopup.addPopupHandler(this);
providerPopup.setSelectedIndex(app.getFileManager()
.getFileProvider() == Provider.LOCAL ? 1 : 0);
}
providerPopup.getElement().getStyle()
.setPosition(com.google.gwt.dom.client.Style.Position.ABSOLUTE);
providerPopup.getElement().getStyle().setLeft(10, Unit.PX);
buttonPanel.add(providerPopup);
buttonPanel.add(listBox);
}
/**
* if user is offline, save local. </br>if user is online and </br>- has
* chosen GOOGLE, {@code uploadToDrive} </br>- material was already saved as
* PUBLIC or SHARED, than only update (API) </br>- material is new or was
* private, than link to GGT
*/
protected void onSave() {
if (app.getFileManager().getFileProvider() == Provider.LOCAL) {
app.getKernel().getConstruction().setTitle(this.title.getText());
app.getFileManager().export(app);
}else if (app.isOffline() || !app.getLoginOperation().isLoggedIn()) {
saveLocal();
} else if (app.getFileManager().getFileProvider() == Provider.GOOGLE) {
uploadToDrive();
}else {
if (app.getActiveMaterial() == null
|| isMacro()) {
app.setActiveMaterial(new Material(0, saveType));
}
switch (listBox.getSelectedIndex()) {
case 0:
app.getActiveMaterial().setVisibility(
Visibility.Private.getToken());
break;
case 1:
app.getActiveMaterial().setVisibility(
Visibility.Shared.getToken());
break;
case 2:
app.getActiveMaterial().setVisibility(
Visibility.Public.getToken());
break;
default:
app.getActiveMaterial().setVisibility(
Visibility.Private.getToken());
break;
}
uploadToGgt(app.getActiveMaterial().getVisibility());
}
}
/**
* sets the application as "saved" and closes the dialog
*/
protected void onDontSave() {
hide();
if (isWorksheet()) {
app.setSaved();
runAfterSaveCallback();
}
}
private void saveLocal() {
ToolTipManagerW.sharedInstance().showBottomMessage(
loc.getMenu("Saving"), false, app);
if (!this.title.getText().equals(
app.getKernel().getConstruction().getTitle())) {
app.resetUniqueId();
app.setLocalID(-1);
}
app.getKernel().getConstruction().setTitle(this.title.getText());
app.getGgbApi().getBase64(true, new StringHandler() {
@Override
public void handle(String s) {
((FileManager) app.getFileManager()).saveFile(s,
getCurrentTimestamp(app), new SaveCallback(app,
SaveState.OK) {
@Override
public void onSaved(final Material mat,
final boolean isLocal) {
super.onSaved(mat, isLocal);
runAfterSaveCallback();
}
});
hide();
}
});
}
// /**
// * @return true if material was already public or shared
// */
// private boolean isAlreadyPublicOrShared() {
// return app.getActiveMaterial().getVisibility()
// .equals(Visibility.Public.getToken())
// || app.getActiveMaterial().getVisibility()
// .equals(Visibility.Shared.getToken());
// }
/**
* Handles the upload of the file and closes the dialog. If there are
* sync-problems with a file, a new one is generated on ggt.
*/
private void uploadToGgt(final String visibility) {
final boolean titleChanged = !SaveDialogW.this.title.getText().equals(
app.getKernel().getConstruction().getTitle());
if (this.saveType != MaterialType.ggt) {
app.getKernel().getConstruction().setTitle(this.title.getText());
}
final StringHandler handler = new StringHandler() {
@Override
public void handle(String base64) {
if (titleChanged
&& isWorksheet()) {
Log.debug("SAVE filename changed");
app.updateMaterialURL(0, null);
doUploadToGgt(app.getTubeId(), visibility, base64,
initMaterialCB(base64, false));
} else if (app.getTubeId() == 0
|| isMacro()) {
Log.debug("SAVE had no Tube ID or tool is saved");
doUploadToGgt(0, visibility, base64,
initMaterialCB(base64, false));
} else {
handleSync(base64, visibility);
}
}
};
ToolTipManagerW.sharedInstance().showBottomMessage(
loc.getMenu("Saving"), false, app);
if (saveType == MaterialType.ggt) {
app.getGgbApi().getMacrosBase64(true, handler);
} else {
app.getGgbApi().getBase64(true, handler);
}
hide();
}
private void uploadToDrive() {
ToolTipManagerW.sharedInstance().showBottomMessage(
loc.getMenu("Saving"), false, app);
app.getGoogleDriveOperation().afterLogin(new Runnable() {
@Override
public void run() {
doUploadToDrive();
}
});
}
/**
* GoogleDrive upload
*/
void doUploadToDrive() {
String saveName = this.title.getText();
String prefix = saveType == MaterialType.ggb ? ".ggb" : ".ggt";
if (!saveName.endsWith(prefix)) {
app.getKernel().getConstruction().setTitle(saveName);
saveName += prefix;
} else {
app.getKernel()
.getConstruction()
.setTitle(
saveName.substring(0,
saveName.length() - prefix.length()));
}
JavaScriptObject callback = ((GoogleDriveOperationW) app
.getGoogleDriveOperation()).getPutFileCallback(saveName,
"GeoGebra", saveType == MaterialType.ggb);
if (saveType == MaterialType.ggt) {
app.getGgbApi().getMacrosBase64(true, callback);
} else {
app.getGgbApi().getBase64(true, callback);
}
}
/**
* @param base64
* material base64
* @param visibility
* "P" / "O" / "S"
*/
void handleSync(final String base64, final String visibility) {
((GeoGebraTubeAPIW) app.getLoginOperation().getGeoGebraTubeAPI())
.getItem(app.getTubeId() + "", new MaterialCallback() {
@Override
public void onLoaded(final List<Material> parseResponse,
ArrayList<Chapter> meta) {
MaterialCallback materialCallback;
if (parseResponse.size() == 1) {
if (parseResponse.get(0).getModified() > app
.getSyncStamp()) {
Log.debug("SAVE MULTIPLE"
+ parseResponse.get(0).getModified()
+ ":" + app.getSyncStamp());
app.updateMaterialURL(0, null);
materialCallback = initMaterialCB(base64, true);
} else {
materialCallback = initMaterialCB(base64, false);
}
doUploadToGgt(app.getTubeId(), visibility, base64,
materialCallback);
} else {
// if the file was deleted meanwhile
// (parseResponse.size() == 0)
app.resetUniqueId();
materialCallback = initMaterialCB(base64, false);
doUploadToGgt(app.getTubeId(), visibility, base64,
materialCallback);
}
}
@Override
public void onError(final Throwable exception) {
// TODO show correct message
app.showError("Error");
}
});
}
/**
* does the upload of the actual opened file to GeoGebraTube
*
* @param tubeID
* id in materials platform
* @param visibility
* visibility string
* @param base64
* material base64
*
* @param materialCallback
* {@link MaterialCallback}
*/
void doUploadToGgt(int tubeID, String visibility, String base64,
MaterialCallback materialCallback) {
((GeoGebraTubeAPIW) app.getLoginOperation().getGeoGebraTubeAPI())
.uploadMaterial(tubeID, visibility, this.title.getText(),
base64, materialCallback, this.saveType);
}
@Override
public void show() {
this.setAnimationEnabled(false);
super.show();
app.invokeLater(new Runnable() {
@Override
public void run() {
position();
}
});
this.setTitle();
if (app.isOffline()) {
this.providerPopup.setVisible(this.supportedProviders
.contains(Provider.LOCAL));
} else {
this.providerPopup.setVisible(true);
this.providerPopup.setSelectedIndex(this.supportedProviders
.indexOf(app.getFileManager().getFileProvider()));
// app.getFileManager().setFileProvider(
// org.geogebra.common.move.ggtapi.models.Material.Provider.TUBE);
if (app.getActiveMaterial() != null) {
if (app.getActiveMaterial().getVisibility()
.equals(Visibility.Public.getToken())) {
this.listBox.setSelectedIndex(Visibility.Public.getIndex());
} else if (app.getActiveMaterial().getVisibility()
.equals(Visibility.Shared.getToken())) {
this.listBox.setSelectedIndex(Visibility.Shared.getIndex());
} else {
this.listBox
.setSelectedIndex(Visibility.Private.getIndex());
}
} else {
this.listBox.setSelectedIndex(defaultVisibility.getIndex());
}
}
listBox.setVisible(app.getFileManager().getFileProvider() == Provider.TUBE);
if (this.title.getText().length() < MIN_TITLE_LENGTH) {
this.saveButton.setEnabled(false);
}
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
title.setFocus(true);
}
});
}
/**
* shows the {@link SaveDialogW} if there are unsaved changes before editing
* another file or creating a new one
*
* Never shown in embedded LAF (Mix, SMART)
*
* @param runnable
* runs either after saved successfully or immediately if dialog
* not needed {@link Runnable}
*/
public void showIfNeeded(Runnable runnable) {
showIfNeeded(runnable, !app.isSaved());
}
/**
* @param runnable
* callback
* @param needed
* whether it's needed to save (otherwise just run callback)
*/
public void showIfNeeded(Runnable runnable, boolean needed) {
if (needed && !app.getLAF().isEmbedded()) {
runAfterSave = runnable;
center();
position();
} else {
runAfterSave = null;
runnable.run();
}
}
/**
* Like center, but more to the top
*/
protected void position() {
int left = (getRootPanel().getOffsetWidth() - getOffsetWidth()) >> 1;
int top = Math.min(
(getRootPanel().getOffsetHeight() - getOffsetHeight()) >> 1,
100);
setPopupPosition(Math.max(left, 0), Math.max(top, 0));
}
private void setTitle() {
String consTitle = app.getKernel().getConstruction().getTitle();
if (consTitle != null && !"".equals(consTitle)
&& !isMacro()) {
if (consTitle.startsWith(MaterialsManager.FILE_PREFIX)) {
consTitle = getTitleOnly(consTitle);
}
this.title.setText(consTitle);
} else {
this.title.setText(loc.getMenu("Untitled"));
this.title.setSelectionRange(0, this.title.getText().length());
}
}
private static String getTitleOnly(String key) {
return key.substring(key.indexOf("_", key.indexOf("_") + 1) + 1);
}
/**
* Update localization
*/
public void setLabels() {
this.getCaption().setText(loc.getMenu("Save"));
this.titleLabel.setText(loc.getMenu("Title") + ": ");
this.dontSaveButton.setText(loc.getMenu("DontSave"));
this.saveButton.setText(loc.getMenu("Save"));
this.listBox.setItemText(Visibility.Private.getIndex(),
loc.getMenu("Private"));
this.listBox.setItemText(Visibility.Shared.getIndex(),
loc.getMenu("Shared"));
this.listBox.setItemText(Visibility.Public.getIndex(),
loc.getMenu("Public"));
}
/**
* runs the callback
*/
public void runAfterSaveCallback() {
if (runAfterSave != null) {
runAfterSave.run();
resetCallback();
}
}
/**
* resets the callback
*/
void resetCallback() {
this.runAfterSave = null;
}
@Override
public void fireActionPerformed(PopupMenuButtonW actionButton) {
Provider provider = this.supportedProviders.get(actionButton.getSelectedIndex());
app.getFileManager().setFileProvider(provider);
listBox.setVisible(provider == Provider.TUBE);
providerPopup.getMyPopup().hide();
}
@Override
public void renderEvent(BaseEvent event) {
if (event instanceof LoginEvent || event instanceof LogOutEvent) {
this.setAvailableProviders();
}
}
/**
* @param saveType
* set the saveType for the SaveDialog
*/
public void setSaveType(MaterialType saveType) {
this.saveType = saveType;
}
/**
* @return true if the MaterialType is ggb
*/
boolean isWorksheet() {
return saveType.equals(MaterialType.ggb);
}
/**
* @return true if the MaterialType is ggt
*/
boolean isMacro() {
return saveType.equals(MaterialType.ggt);
}
/**
* @param visibility
* new default
* @return this
*/
public SaveDialogW setDefaultVisibility(Visibility visibility) {
this.defaultVisibility = visibility;
return this;
}
}