package org.syncany.gui.history;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
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.events.SelectionListener;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Scale;
import org.ocpsoft.prettytime.PrettyTime;
import org.syncany.config.GuiEventBus;
import org.syncany.database.DatabaseVersionHeader;
import org.syncany.gui.Panel;
import org.syncany.gui.history.events.ModelSelectedDateUpdatedEvent;
import org.syncany.gui.history.events.ModelSelectedRootUpdatedEvent;
import org.syncany.gui.util.I18n;
import org.syncany.gui.util.SWTResourceManager;
import org.syncany.operations.daemon.Watch;
import org.syncany.operations.daemon.messages.GetDatabaseVersionHeadersFolderRequest;
import org.syncany.operations.daemon.messages.GetDatabaseVersionHeadersFolderResponse;
import org.syncany.operations.daemon.messages.ListWatchesManagementRequest;
import org.syncany.operations.daemon.messages.ListWatchesManagementResponse;
import com.google.common.base.Objects;
import com.google.common.eventbus.Subscribe;
/**
* The main panel displays the history of a managed folder in form of
* a file tree ({@link FileTreeComposite}) and a log view ({@link LogComposite}).
* The panel gives the user the possibility to select the folder to browse, and
* offers a date slider to choose the date at which to browse the history.
*
* <p>The panel sends out {@link ListWatchesManagementRequest}s to retrieve the
* daemon-managed folders (roots), and {@link GetDatabaseVersionHeadersFolderRequest}s
* to get the dates of which database versions exist. The file tree and log composites
* update themselves using other requests.
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class MainPanel extends Panel {
private static final Logger logger = Logger.getLogger(MainPanel.class.getSimpleName());
private static final String IMAGE_RESOURCE_FORMAT = "/" + MainPanel.class.getPackage().getName().replace('.', '/') + "/%s.png";
private static final int DATE_SLIDER_DELAY = 400;
private HistoryModel historyModel;
private HistoryDialog historyDialog;
private ListWatchesManagementRequest pendingListWatchesRequest;
private GuiEventBus eventBus;
private boolean dateLabelPrettyTime;
private AtomicInteger dateSliderValue;
private Timer dateSliderChangeTimer;
private List<DatabaseVersionHeader> dateSliderHeaders;
private Combo rootSelectCombo;
private SelectionListener rootSelectComboListener;
private Label dateLabel;
private Scale dateSlider;
private StackLayout stackLayout;
private Composite stackComposite;
private FileTreeComposite fileTreeComposite;
private LogComposite logComposite;
private LoadingComposite loadingComposite;
private Button toggleTreeButton;
private Button toggleLogButton;
public MainPanel(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.pendingListWatchesRequest = null;
this.eventBus = GuiEventBus.getAndRegister(this);
this.dateLabelPrettyTime = true;
this.dateSliderValue = new AtomicInteger(-1);
this.dateSliderChangeTimer = null;
this.dateSliderHeaders = Collections.synchronizedList(new ArrayList<DatabaseVersionHeader>());
createMainComposite();
createToggleButtons();
createRootSelectionCombo();
createRootSelectionComboListener();
createDateSlider();
createStackComposite();
createComposites();
showLoadingComposite();
sendListWatchesRequest();
}
private void createMainComposite() {
GridLayout mainCompositeGridLayout = new GridLayout(5, false);
mainCompositeGridLayout.marginTop = 0;
mainCompositeGridLayout.marginLeft = 0;
mainCompositeGridLayout.marginRight = 0;
setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 5, 1));
setLayout(mainCompositeGridLayout);
}
private void createToggleButtons() {
toggleLogButton = new Button(this, SWT.TOGGLE);
toggleLogButton.setEnabled(false);
toggleLogButton.setSelection(true);
toggleLogButton.setImage(SWTResourceManager.getImage(String.format(IMAGE_RESOURCE_FORMAT, "log")));
toggleTreeButton = new Button(this, SWT.TOGGLE);
toggleTreeButton.setEnabled(false);
toggleTreeButton.setSelection(false);
toggleTreeButton.setImage(SWTResourceManager.getImage(String.format(IMAGE_RESOURCE_FORMAT, "tree")));
toggleLogButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
showLog();
}
});
toggleTreeButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
showTree();
}
});
}
private void createRootSelectionCombo() {
rootSelectCombo = new Combo(this, SWT.DROP_DOWN | SWT.BORDER | SWT.READ_ONLY);
rootSelectCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
rootSelectCombo.setText(I18n.getText("org.syncany.gui.history.HistoryDialog.retrievingList"));
rootSelectCombo.setEnabled(false);
}
private void createRootSelectionComboListener() {
rootSelectComboListener = new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onRootSelectComboSelected();
}
};
}
private void createDateSlider() {
// Label
GridData dateLabelGridData = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
dateLabelGridData.minimumWidth = 150;
dateLabel = new Label(this, SWT.CENTER);
dateLabel.setEnabled(false);
dateLabel.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_HAND));
dateLabel.setLayoutData(dateLabelGridData);
dateLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
dateLabelPrettyTime = !dateLabelPrettyTime;
if (dateLabel.getData() != null) {
setDateLabel((Date) dateLabel.getData());
}
}
});
// Slider
dateSlider = new Scale(this, SWT.HORIZONTAL | SWT.BORDER);
dateSlider.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
dateSlider.setEnabled(false);
dateSlider.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onDateSliderSelected();
}
});
}
private void createStackComposite() {
stackLayout = new StackLayout();
stackLayout.marginHeight = 0;
stackLayout.marginWidth = 0;
GridData stackCompositeGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 5, 1);
stackCompositeGridData.minimumWidth = 500;
stackComposite = new Composite(this, SWT.DOUBLE_BUFFERED);
stackComposite.setLayout(stackLayout);
stackComposite.setLayoutData(stackCompositeGridData);
}
private void createComposites() {
loadingComposite = new LoadingComposite(stackComposite, SWT.NONE);
fileTreeComposite = new FileTreeComposite(stackComposite, SWT.NONE, historyModel, historyDialog);
logComposite = new LogComposite(stackComposite, SWT.NONE, historyModel, this);
}
/**
* Displays the log composite, toggles the buttons
* and disposes the loading composite.
*/
public void showLog() {
setCurrentControl(logComposite);
enableControls();
toggleButtons(true);
disposeLoadingComposite();
}
/**
* Displays the file tree composite, toggles the buttons
* and disposes the loading composite.
*/
public void showTree() {
setCurrentControl(fileTreeComposite);
enableControls();
toggleButtons(false);
disposeLoadingComposite();
}
private void setCurrentControl(Control control) {
stackLayout.topControl = control;
stackComposite.layout();
}
@SuppressWarnings("unchecked")
private void onRootSelectComboSelected() {
List<Watch> watches = (List<Watch>) rootSelectCombo.getData();
if (watches != null) {
int selectionIndex = rootSelectCombo.getSelectionIndex();
if (selectionIndex >= 0 && selectionIndex < watches.size()) {
String newRoot = watches.get(selectionIndex).getFolder().getAbsolutePath();
historyModel.reset();
historyModel.setSelectedRoot(newRoot);
sendGetDatabaseVersionHeadersFolderRequest(newRoot);
}
}
}
private void setDateLabel(final Date dateSliderDate) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
String dateStrPretty = new PrettyTime().format(dateSliderDate);
String dateStrExact = dateSliderDate.toString();
dateLabel.setData(dateSliderDate);
if (dateLabelPrettyTime) {
dateLabel.setText(dateStrPretty);
dateLabel.setToolTipText(dateStrExact);
}
else {
dateLabel.setText(dateStrExact);
dateLabel.setToolTipText(dateStrPretty);
}
}
});
}
private void onDateSliderSelected() {
synchronized (dateSlider) {
int newDateSliderValue = dateSlider.getSelection();
Date newSliderDate = getSliderDate();
boolean dateSliderValueChanged = dateSliderValue.get() != newDateSliderValue;
if (dateSliderValueChanged) {
// Update cached value
dateSliderValue.set(newDateSliderValue);
// Update label right away
setDateLabel(newSliderDate);
logComposite.highlightByDate(newSliderDate);
// Update file tree after a while
if (dateSliderChangeTimer != null) {
dateSliderChangeTimer.cancel();
}
dateSliderChangeTimer = new Timer();
dateSliderChangeTimer.schedule(createDateSliderTimerTask(), DATE_SLIDER_DELAY);
logger.log(Level.INFO, "Main: Date slider value changed to " + newSliderDate + "; setting timer to refresh views in 800ms ...");
}
}
}
private TimerTask createDateSliderTimerTask() {
return new TimerTask() {
@Override
public void run() {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
logger.log(Level.INFO, "Main: Date slider timer fired.");
onDateChanged(getSliderDate());
}
});
}
};
}
private Date getSliderDate() {
int dateSelectionIndex = dateSlider.getSelection();
if (dateSelectionIndex >= 0 && dateSelectionIndex < dateSliderHeaders.size()) {
return dateSliderHeaders.get(dateSelectionIndex).getDate();
}
else {
return null;
}
}
private void setSliderDate(Date newDate) {
for (int i = 0; i < dateSliderHeaders.size(); i++) {
DatabaseVersionHeader header = dateSliderHeaders.get(i);
if (header.getDate().equals(newDate)) {
dateSlider.setSelection(i);
}
}
}
private void showLoadingComposite() {
setCurrentControl(loadingComposite);
}
private void enableControls() {
toggleLogButton.setEnabled(true);
toggleTreeButton.setEnabled(true);
rootSelectCombo.setEnabled(true);
dateLabel.setEnabled(true);
dateSlider.setEnabled(true);
}
private void toggleButtons(boolean logEnabled) {
toggleLogButton.setSelection(logEnabled);
toggleTreeButton.setSelection(!logEnabled);
}
private void disposeLoadingComposite() {
if (!loadingComposite.isDisposed()) {
loadingComposite.dispose();
}
}
@Override
public boolean validatePanel() {
return true;
}
private void updateSlider(final List<DatabaseVersionHeader> headers) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
if (headers.size() > 0) {
int maxValue = headers.size() - 1;
Date newSelectedDate = headers.get(headers.size()-1).getDate();
dateSliderHeaders.clear();
dateSliderHeaders.addAll(headers);
dateSlider.setMinimum(0);
dateSlider.setMaximum(maxValue);
dateSlider.setSelection(maxValue);
setDateLabel(newSelectedDate);
// setMaximum does not actually work if it sets 0, hence we disable the slider.
if (headers.size() == 1) {
dateSlider.setEnabled(false);
}
}
else {
// Disable slider and set today as date to clear fields
dateSlider.setSelection(0);
dateSlider.setEnabled(false);
setDateLabel(new Date());
}
}
});
}
private void sendListWatchesRequest() {
pendingListWatchesRequest = new ListWatchesManagementRequest();
eventBus.post(pendingListWatchesRequest);
}
@Subscribe
public void onListWatchesManagementResponse(final ListWatchesManagementResponse listWatchesResponse) {
if (pendingListWatchesRequest != null && pendingListWatchesRequest.getId() == listWatchesResponse.getRequestId()) {
// Nullify pending request
pendingListWatchesRequest = null;
// Update combo box
updateRootsCombo(listWatchesResponse.getWatches());
}
}
private void updateRootsCombo(final ArrayList<Watch> watches) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
rootSelectCombo.removeSelectionListener(rootSelectComboListener);
rootSelectCombo.removeAll();
for (Watch watch : watches) {
rootSelectCombo.add(watch.getFolder().getName());
}
rootSelectCombo.setData(watches);
if (rootSelectCombo.getItemCount() > 0) {
historyModel.reset();
rootSelectCombo.addSelectionListener(rootSelectComboListener);
rootSelectCombo.select(0);
onRootSelectComboSelected();
}
}
});
}
@Subscribe
public void onModelSelectedRootUpdatedEvent(final ModelSelectedRootUpdatedEvent event) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
sendGetDatabaseVersionHeadersFolderRequest(event.getSelectedRoot());
}
});
}
private void sendGetDatabaseVersionHeadersFolderRequest(String newRoot) {
GetDatabaseVersionHeadersFolderRequest getHeadersRequest = new GetDatabaseVersionHeadersFolderRequest();
getHeadersRequest.setRoot(newRoot);
eventBus.post(getHeadersRequest);
}
@Subscribe
public void onGetDatabaseVersionHeadersFolderResponse(final GetDatabaseVersionHeadersFolderResponse getHeadersResponse) {
List<DatabaseVersionHeader> headers = getHeadersResponse.getDatabaseVersionHeaders();
if (headers.size() > 0) {
Date newSelectedDate = headers.get(headers.size()-1).getDate();
historyModel.setSelectedDate(newSelectedDate);
}
else {
historyModel.setSelectedDate(null);
}
updateSlider(headers);
}
@Subscribe
public void onModelSelectedDateUpdatedEvent(final ModelSelectedDateUpdatedEvent event) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
onDateChanged(event.getSelectedDate());
if (!Objects.equal(event.getSelectedDate(), getSliderDate())) {
setSliderDate(event.getSelectedDate());
}
}
});
}
private void onDateChanged(Date newDate) {
boolean listUpdateRequired = !newDate.equals(historyModel.getSelectedDate());
if (listUpdateRequired) {
logger.log(Level.INFO, "Main: Changing DATE model in model to " + newDate + " ...");
historyModel.setSelectedDate(newDate);
}
}
public void dispose() {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
eventBus.unregister(MainPanel.this);
if (!logComposite.isDisposed()) {
logComposite.dispose();
}
if (!fileTreeComposite.isDisposed()) {
fileTreeComposite.dispose();
}
if (!loadingComposite.isDisposed()) {
loadingComposite.dispose();
}
}
});
super.dispose();
}
}