package org.syncany.gui.history;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
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.Cursor;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.syncany.config.GuiEventBus;
import org.syncany.database.FileVersion;
import org.syncany.database.FileVersion.FileStatus;
import org.syncany.database.FileVersion.FileType;
import org.syncany.database.PartialFileHistory;
import org.syncany.database.PartialFileHistory.FileHistoryId;
import org.syncany.gui.Panel;
import org.syncany.gui.util.DesktopUtil;
import org.syncany.gui.util.I18n;
import org.syncany.gui.util.SWTResourceManager;
import org.syncany.gui.util.WidgetDecorator;
import org.syncany.operations.daemon.messages.LsFolderRequest;
import org.syncany.operations.daemon.messages.LsFolderResponse;
import org.syncany.operations.daemon.messages.RestoreFolderRequest;
import org.syncany.operations.daemon.messages.RestoreFolderResponse;
import org.syncany.operations.ls.LsOperationOptions;
import org.syncany.operations.restore.RestoreOperationOptions;
import org.syncany.operations.restore.RestoreOperationResult;
import org.syncany.operations.restore.RestoreOperationResult.RestoreResultCode;
import org.syncany.util.EnvironmentUtil;
import org.syncany.util.FileUtil;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.eventbus.Subscribe;
/**
* The detail panel shows a list of all the {@link FileVersion}s of
* a single {@link PartialFileHistory}. In addition, it gives the user
* the possibility to restore certain file versions.
*
* <p>The data for this view is retrieved by a {@link LsFolderRequest} (and
* the corresponding {@link LsFolderResponse}) for a single file. The restore
* operation is triggered by sending a {@link RestoreFolderRequest} to the
* daemon.
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class DetailPanel extends Panel {
private static final Logger logger = Logger.getLogger(DetailPanel.class.getSimpleName());
private static final String IMAGE_RESOURCE_FORMAT = "/" + DetailPanel.class.getPackage().getName().replace('.', '/') + "/%s.png";
private static final String IMAGE_LOADING_SPINNER_RESOURCE = "/" + DetailPanel.class.getPackage().getName().replace('.', '/') + "/loading-spinner.gif";
private static final int IMAGE_LOADING_SPINNER_FRAME_RATE = 90; // ms per image
private static final int RESTORE_FILENAME_SHORTENED_LENGTH = 50;
private static final int COLUMN_INDEX_STATUS = 0;
private static final int COLUMN_INDEX_PATH = 1;
private static final int COLUMN_INDEX_VERSION = 2;
private static final int COLUMN_INDEX_TYPE = 3;
private static final int COLUMN_INDEX_SIZE = 4;
private static final int COLUMN_INDEX_POSIX_PERMS = 5;
private static final int COLUMN_INDEX_DOS_ATTRS = 6;
private static final int COLUMN_INDEX_CHECKSUM = 7;
private static final int COLUMN_INDEX_LAST_MODIFIED = 8;
private static final int COLUMN_INDEX_UPDATED = 9;
private HistoryModel historyModel;
private HistoryDialog historyDialog;
private Map<Integer, LsFolderRequest> pendingLsFolderRequests;
private RestoreFolderRequest pendingRestoreRequest;
private GuiEventBus eventBus;
private Composite restoreStatusComposite;
private ImageComposite restoreStatusIconComposite;
private Label restoreStatusTextLabel;
private Button restoreButton;
private Table historyTable;
private File restoredFile;
private PartialFileHistory selectedFileHistory;
public DetailPanel(Composite composite, int style, HistoryModel historyModel, HistoryDialog historyDialog) {
super(composite, style);
this.setBackgroundImage(null);
this.setBackgroundMode(SWT.INHERIT_DEFAULT);
this.historyModel = historyModel;
this.historyDialog = historyDialog;
this.pendingRestoreRequest = null;
this.pendingLsFolderRequests = Maps.newConcurrentMap();
this.eventBus = GuiEventBus.getAndRegister(this);
this.restoreStatusIconComposite = null;
this.restoreStatusTextLabel = null;
this.restoreButton = null;
this.historyTable = null;
this.createContents();
}
/**
* Resets the panel to the given file history, by sending an {@link LsFolderResponse}
* and resetting the user interface.
*/
public void resetPanel(String root, FileHistoryId fileHistoryId) {
logger.log(Level.INFO, "Detail panel: Showing detail panel for file history ID #" + fileHistoryId + " ...");
hideRestoreStatusLabel();
clearHistoryTable();
sendLsFolderRequest(root, fileHistoryId);
}
private void createContents() {
createMainComposite();
createButtonRow();
createHistoryTable();
createHistoryTableListener();
createHistoryTableColumns();
}
private void createMainComposite() {
logger.log(Level.INFO, "Detail panel: Creating main composite ...");
GridLayout mainCompositeGridLayout = new GridLayout(3, false);
mainCompositeGridLayout.marginTop = 0;
mainCompositeGridLayout.marginLeft = 0;
mainCompositeGridLayout.marginRight = 0;
setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));
setLayout(mainCompositeGridLayout);
}
private void createButtonRow() {
logger.log(Level.INFO, "Detail panel: Creating button row ...");
Button backButton = new Button(this, SWT.NONE);
backButton.setText(I18n.getText("org.syncany.gui.history.DetailPanel.button.back"));
backButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1));
backButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
historyDialog.showMainPanel();
}
});
GridData restoreStatusCompositeGridData = new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1);
restoreStatusCompositeGridData.verticalIndent = 3;
restoreStatusComposite = new Composite(this, SWT.NONE);
restoreStatusComposite.setLayout(new GridLayout(2, false));
restoreStatusComposite.setLayoutData(restoreStatusCompositeGridData);
restoreStatusIconComposite = new ImageComposite(restoreStatusComposite, SWT.NONE);
restoreStatusIconComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
restoreStatusTextLabel = new Label(restoreStatusComposite, SWT.NONE);
restoreStatusTextLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1));
restoreStatusTextLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
if (restoredFile != null) {
DesktopUtil.launch(restoredFile.getAbsolutePath());
}
}
});
restoreButton = new Button(this, SWT.NONE);
restoreButton.setEnabled(false);
restoreButton.setText(I18n.getText("org.syncany.gui.history.DetailPanel.button.restore"));
restoreButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
restoreButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
restoreSelectedFile();
}
});
}
private void createHistoryTable() {
logger.log(Level.INFO, "Detail panel: Creating history table ...");
GridData historyTableGridData = new GridData(SWT.FILL, SWT.FILL, true, true);
historyTableGridData.verticalIndent = 5;
historyTableGridData.horizontalIndent = 0;
historyTableGridData.horizontalSpan = 3;
historyTable = new Table(this, SWT.BORDER | SWT.FULL_SELECTION);
historyTable.setHeaderVisible(true);
historyTable.setLayoutData(historyTableGridData);
if (EnvironmentUtil.isWindows()) {
historyTable.setBackground(WidgetDecorator.WHITE);
}
}
private void createHistoryTableListener() {
logger.log(Level.INFO, "Detail panel: Creating history table listeners ...");
historyTable.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
TableItem tableItem = (TableItem) e.item;
FileVersion fileVersion = (FileVersion) tableItem.getData();
selectFileVersion(fileVersion);
}
});
historyTable.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
resizeColumns();
}
});
}
private void selectFileVersion(FileVersion fileVersion) {
boolean isLastVersion = fileVersion.equals(selectedFileHistory.getLastVersion());
boolean restoreInProgress = pendingRestoreRequest != null;
boolean restoreButtonEnabled = !isLastVersion && !restoreInProgress;
logger.log(Level.INFO, "Detail panel: Selecting file version" + fileVersion.getVersion() + "; Restore in progress = " + restoreInProgress + "; Last/current version = " + isLastVersion + " --> Button enabled = " + restoreButtonEnabled);
restoreButton.setEnabled(restoreButtonEnabled);
}
private void createHistoryTableColumns() {
logger.log(Level.INFO, "Detail panel: Creating history table columns ...");
// When reordering/adding columns, make sure to adjust the constants!
// e.g TABLE_COLUMN_REMOTE_VERSION, ...
TableColumn columnStatus = new TableColumn(historyTable, SWT.LEFT);
columnStatus.setWidth(30);
TableColumn columnPath = new TableColumn(historyTable, SWT.NONE);
columnPath.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.path"));
columnPath.setWidth(210);
TableColumn columnVersion = new TableColumn(historyTable, SWT.NONE);
columnVersion.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.version"));
columnVersion.setWidth(30);
TableColumn columnType = new TableColumn(historyTable, SWT.LEFT);
columnType.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.type"));
columnType.setWidth(60);
TableColumn columnSize = new TableColumn(historyTable, SWT.LEFT);
columnSize.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.size"));
columnSize.setWidth(70);
TableColumn columnPosixPermissions = new TableColumn(historyTable, SWT.LEFT);
columnPosixPermissions.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.posixPermissions"));
columnPosixPermissions.setWidth(70);
TableColumn columnDosAttributes = new TableColumn(historyTable, SWT.LEFT);
columnDosAttributes.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.dosAttributes"));
columnDosAttributes.setWidth(70);
TableColumn columnChecksum = new TableColumn(historyTable, SWT.LEFT);
columnChecksum.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.checksum"));
columnChecksum.setWidth(200);
TableColumn columnLastModified = new TableColumn(historyTable, SWT.LEFT);
columnLastModified.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.lastModified"));
columnLastModified.setWidth(130);
TableColumn columnUpdated = new TableColumn(historyTable, SWT.LEFT);
columnUpdated.setText(I18n.getText("org.syncany.gui.history.DetailPanel.table.updated"));
columnUpdated.setWidth(130);
}
private void updateTable(LsFolderRequest lsRequest, LsFolderResponse lsResponse) {
logger.log(Level.INFO, "Detail panel: Updating table with " + lsResponse.getResult().getFileVersions().size() + " file versions ...");
// Add new file version items
List<PartialFileHistory> fileVersions = new ArrayList<>(lsResponse.getResult().getFileVersions().values());
selectedFileHistory = fileVersions.get(0);
for (FileVersion fileVersion : selectedFileHistory.getFileVersions().values()) {
String checksumStr = (fileVersion.getChecksum() != null) ? fileVersion.getChecksum().toString() : "";
TableItem tableItem = new TableItem(historyTable, SWT.NONE);
tableItem.setData(fileVersion);
tableItem.setImage(COLUMN_INDEX_STATUS, getStatusImage(fileVersion.getStatus()));
tableItem.setText(COLUMN_INDEX_PATH, fileVersion.getPath());
tableItem.setText(COLUMN_INDEX_VERSION, Long.toString(fileVersion.getVersion()));
tableItem.setText(COLUMN_INDEX_TYPE, fileVersion.getType().toString());
tableItem.setText(COLUMN_INDEX_SIZE, FileUtil.formatFileSize(fileVersion.getSize()));
tableItem.setText(COLUMN_INDEX_POSIX_PERMS, fileVersion.getPosixPermissions());
tableItem.setText(COLUMN_INDEX_DOS_ATTRS, fileVersion.getDosAttributes());
tableItem.setText(COLUMN_INDEX_CHECKSUM, checksumStr);
tableItem.setText(COLUMN_INDEX_LAST_MODIFIED, ""+fileVersion.getLastModified());
tableItem.setText(COLUMN_INDEX_UPDATED, ""+fileVersion.getUpdated());
}
if (historyTable.getItemCount() > 0) {
restoreButton.setEnabled(false);
historyTable.select(historyTable.getItemCount()-1);
}
resizeColumns();
}
private void resizeColumns() {
logger.log(Level.INFO, "Detail panel: Auto-resizing table columns ...");
for (TableColumn tableColumn : historyTable.getColumns()) {
tableColumn.pack();
}
historyTable.layout();
}
private Image getStatusImage(FileStatus status) {
switch (status) {
case NEW:
return SWTResourceManager.getImage(String.format(IMAGE_RESOURCE_FORMAT, "add"));
case CHANGED:
case RENAMED:
return SWTResourceManager.getImage(String.format(IMAGE_RESOURCE_FORMAT, "edit"));
case DELETED:
return SWTResourceManager.getImage(String.format(IMAGE_RESOURCE_FORMAT, "delete"));
default:
return null;
}
}
@Override
public boolean validatePanel() {
return true;
}
private void hideRestoreStatusLabel() {
logger.log(Level.INFO, "Detail panel: Hiding restore status label ...");
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
restoreStatusTextLabel.setVisible(false);
restoreStatusIconComposite.setVisible(false);
}
});
}
private void clearHistoryTable() {
logger.log(Level.INFO, "Detail panel: Clearing history table ...");
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
historyTable.removeAll();
}
});
}
private void sendLsFolderRequest(String root, FileHistoryId fileHistoryId) {
// Create list request
LsOperationOptions lsOptions = new LsOperationOptions();
lsOptions.setPathExpression(fileHistoryId.toString());
lsOptions.setFileHistoryId(true);
lsOptions.setRecursive(false);
lsOptions.setDeleted(true);
lsOptions.setFetchHistories(true);
lsOptions.setFileTypes(Sets.newHashSet(FileType.FILE, FileType.SYMLINK));
LsFolderRequest lsRequest = new LsFolderRequest();
lsRequest.setRoot(root);
lsRequest.setOptions(lsOptions);
logger.log(Level.INFO, "Detail panel: Sending LsRequest with ID #" + lsRequest.getId() + " for " + root + " ...");
// Send request
pendingLsFolderRequests.put(lsRequest.getId(), lsRequest);
eventBus.post(lsRequest);
}
@Subscribe
public void onLsFolderResponse(final LsFolderResponse lsResponse) {
logger.log(Level.INFO, "Detail panel: LsResponse received for request #" + lsResponse.getRequestId() + ".");
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
LsFolderRequest lsRequest = pendingLsFolderRequests.remove(lsResponse.getRequestId());
if (lsRequest != null) {
updateTable(lsRequest, lsResponse);
}
}
});
}
private void restoreSelectedFile() {
TableItem[] selectedItems = historyTable.getSelection();
if (selectedItems.length > 0) {
TableItem tableItem = selectedItems[0];
FileVersion fileVersion = (FileVersion) tableItem.getData();
restoreFileVersion(fileVersion);
}
}
private void restoreFileVersion(FileVersion fileVersion) {
// Set labels/status
String shortFileName = shortenFileName(fileVersion.getPath());
String versionStr = Long.toString(fileVersion.getVersion());
restoreButton.setEnabled(false);
restoreStatusIconComposite.setVisible(true);
restoreStatusTextLabel.setVisible(true);
restoreStatusIconComposite.setAnimatedImage(IMAGE_LOADING_SPINNER_RESOURCE, IMAGE_LOADING_SPINNER_FRAME_RATE);
restoreStatusTextLabel.setText(I18n.getText("org.syncany.gui.history.DetailPanel.label.fileRestoreOngoing", shortFileName, versionStr));
restoreStatusTextLabel.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_ARROW));
restoreStatusTextLabel.setToolTipText("");
restoredFile = null;
layout();
// Send restore request
RestoreOperationOptions restoreOptions = new RestoreOperationOptions();
restoreOptions.setFileHistoryId(fileVersion.getFileHistoryId());
restoreOptions.setFileVersion(fileVersion.getVersion().intValue());
pendingRestoreRequest = new RestoreFolderRequest();
pendingRestoreRequest.setRoot(historyModel.getSelectedRoot());
pendingRestoreRequest.setOptions(restoreOptions);
eventBus.post(pendingRestoreRequest);
}
private String shortenFileName(String path) {
String baseName = new File(path).getName();
return (baseName.length() >= RESTORE_FILENAME_SHORTENED_LENGTH) ? baseName.substring(0, RESTORE_FILENAME_SHORTENED_LENGTH-3) + "..." : baseName;
}
@Subscribe
public void onRestoreResponseReceived(final RestoreFolderResponse restoreResponse) {
logger.log(Level.INFO, "Detail panel: RestoreResponse received for request #" + restoreResponse.getRequestId() + ".");
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
boolean restoreRequestMatches = pendingRestoreRequest != null
&& pendingRestoreRequest.getId() == restoreResponse.getRequestId();
if (restoreRequestMatches) {
updateRestoreStatus(pendingRestoreRequest, restoreResponse);
pendingRestoreRequest = null;
}
}
});
}
private void updateRestoreStatus(RestoreFolderRequest restoreRequest, RestoreFolderResponse restoreResponse) {
RestoreOperationResult restoreResult = restoreResponse.getResult();
RestoreResultCode restoreResultCode = restoreResult.getResultCode();
// Set labels/status
restoreButton.setEnabled(true);
restoreStatusIconComposite.setVisible(true);
restoreStatusTextLabel.setVisible(true);
if (restoreResultCode == RestoreResultCode.ACK) {
String shortFileName = shortenFileName(restoreResult.getTargetFile().getAbsolutePath());
logger.log(Level.INFO, "Detail panel: Restore successful, file restored to " + restoreResult.getTargetFile().toString());
restoreStatusIconComposite.setImage(SWTResourceManager.getImage(String.format(IMAGE_RESOURCE_FORMAT, "success")));
restoreStatusTextLabel.setText(I18n.getText("org.syncany.gui.history.DetailPanel.label.fileRestoreSuccess", shortFileName));
restoreStatusTextLabel.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_HAND));
restoreStatusTextLabel.setToolTipText(restoreResult.getTargetFile().toString());
restoredFile = restoreResult.getTargetFile();
}
else {
logger.log(Level.WARNING, "Detail panel: Restore FAILED, error code " + restoreResultCode);
restoreStatusIconComposite.setImage(SWTResourceManager.getImage(String.format(IMAGE_RESOURCE_FORMAT, "failure")));
restoreStatusTextLabel.setText(I18n.getText("org.syncany.gui.history.DetailPanel.label.fileRestoreFailure"));
restoreStatusTextLabel.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_ARROW));
restoreStatusTextLabel.setToolTipText("");
restoredFile = null;
}
layout();
}
@Override
public void dispose() {
logger.log(Level.INFO, "Detail panel: Disposing panel ...");
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
eventBus.unregister(DetailPanel.this);
}
});
}
}