package org.syncany.gui.history;
import java.io.File;
import java.util.Date;
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.custom.ScrolledComposite;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.MessageBox;
import org.syncany.config.GuiEventBus;
import org.syncany.gui.history.events.ModelSelectedDateUpdatedEvent;
import org.syncany.gui.history.events.ModelSelectedRootUpdatedEvent;
import org.syncany.gui.util.DesktopUtil;
import org.syncany.gui.util.I18n;
import org.syncany.gui.util.WidgetDecorator;
import org.syncany.operations.daemon.messages.LogFolderRequest;
import org.syncany.operations.daemon.messages.LogFolderResponse;
import org.syncany.operations.log.LightweightDatabaseVersion;
import org.syncany.operations.log.LogOperationOptions;
import org.syncany.operations.log.LogOperationResult;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;
/**
* The log composite displays the results of the log operation ({@link LogOperationResult}).
* Each database version is displayed as a {@link LogTabComposite}. The composite
* only retrieves/displays a certain amount of tabs until the user scrolls to the bottom
* of the component -- at which point the new tabs are loaded. Loading is done via
* {@link LogFolderRequest} (and the corresponding {@link LogFolderResponse}).
*
* <p>The composite furthermore reacts on model changes ({@link ModelSelectedDateUpdatedEvent})
* by resetting the entire component.
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class LogComposite extends Composite {
private static final Logger logger = Logger.getLogger(LogComposite.class.getSimpleName());
public static final int LOG_REQUEST_DATABASE_COUNT = 15;
public static final int LOG_REQUEST_FILE_COUNT = 10;
private HistoryModel historyModel;
private MainPanel mainPanel;
private LogFolderRequest pendingLogFolderRequest;
private GuiEventBus eventBus;
private ScrolledComposite scrollComposite;
private Composite logContentComposite;
private Map<Date, LogTabComposite> tabComposites;
private LogTabComposite highlightedTabComposite;
private List<Composite> loadingTabComposites;
public LogComposite(Composite parent, int style, HistoryModel historyModel, MainPanel mainPanel) {
super(parent, style);
this.historyModel = historyModel;
this.mainPanel = mainPanel;
this.pendingLogFolderRequest = null;
this.eventBus = GuiEventBus.getAndRegister(this);
this.scrollComposite = null;
this.logContentComposite = null;
this.tabComposites = Maps.newConcurrentMap();
this.highlightedTabComposite = null;
this.loadingTabComposites = Lists.newArrayList();
this.createContents();
}
private void createContents() {
createMainComposite();
createScrollComposite();
replaceScrollEventHandling();
redrawAll();
}
private void resetAndDisposeAll() {
logger.log(Level.INFO, "Log composite: Resetting tabs and disposing all controls ...");
for (Control control : logContentComposite.getChildren()) {
control.dispose();
}
tabComposites.clear();
highlightedTabComposite = null;
}
private void createMainComposite() {
logger.log(Level.INFO, "Log composite: Creating main composite ...");
GridLayout mainCompositeGridLayout = new GridLayout(3, false);
mainCompositeGridLayout.marginTop = 0;
mainCompositeGridLayout.marginLeft = 0;
mainCompositeGridLayout.marginRight = 0;
mainCompositeGridLayout.horizontalSpacing = 0;
mainCompositeGridLayout.verticalSpacing = 0;
mainCompositeGridLayout.marginHeight = 0;
mainCompositeGridLayout.marginWidth = 0;
setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));
setLayout(mainCompositeGridLayout);
}
private void createScrollComposite() {
logger.log(Level.INFO, "Log composite: Creating scroll composite ...");
GridLayout mainCompositeGridLayout = new GridLayout(1, false);
scrollComposite = new ScrolledComposite(this, SWT.V_SCROLL);
scrollComposite.setLayout(mainCompositeGridLayout);
scrollComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
logContentComposite = new Composite(scrollComposite, SWT.NONE);
logContentComposite.setLayout(mainCompositeGridLayout);
logContentComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
scrollComposite.setExpandVertical(true);
scrollComposite.setExpandHorizontal(true);
scrollComposite.setContent(logContentComposite);
scrollComposite.setShowFocusedControl(true);
}
/**
* Redraws and layouts the entire component (including the scroll component),
* and adjusts the size of the scroll component to reflect the content. Calling
* this method is necessary after tabs have been added/removed.
*/
public void redrawAll() {
logger.log(Level.INFO, "Log composite: Redrawing and layouting scroll composite ...");
logContentComposite.layout();
layout();
scrollComposite.setMinSize(logContentComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT));
scrollComposite.setRedraw(true);
}
/**
* This method highlights a {@link LogTabComposite} by its corresponding
* database version date (and scrolls to that tab). It removes the highlight
* status from all the other tabs. If no matching tabs are found (because they have
* not been loaded), no tab is highlighted.
*/
public synchronized void highlightByDate(Date highlightDate) {
// De-highlight
if (highlightedTabComposite != null) {
highlightedTabComposite.setHighlighted(false);
}
// Highlight new tab
if (highlightDate != null) {
LogTabComposite tabComposite = tabComposites.get(highlightDate);
if (tabComposite != null) {
logger.log(Level.INFO, "Log composite: Highlighting tab with date " + highlightDate);
tabComposite.setHighlighted(true);
tabComposite.setFocus(); // The scroll composite will scroll to it.
highlightedTabComposite = tabComposite;
}
}
}
/**
* Manually scrolls the scroll component by a certain number of pixels. This
* count parameter is multiplied by the vertical scroll increment.
*/
public void scrollBy(int count) {
int increment = scrollComposite.getVerticalBar().getIncrement();
scrollComposite.setOrigin(0, scrollComposite.getOrigin().y - increment*count);
}
private void replaceScrollEventHandling() {
logger.log(Level.INFO, "Log composite: Replacing scroll event handling ...");
// Disables the default scrolling functionality of the ScrolledComposite
// and replaces it by manually scrolling.
Display.getDefault().addFilter(SWT.MouseWheel, new Listener() {
@Override
public void handleEvent(Event e) {
if (e.widget.equals(logContentComposite)) {
e.doit = false;
scrollBy(e.count);
}
}
});
}
@Subscribe
public void onModelSelectedRootUpdatedEvent(ModelSelectedRootUpdatedEvent event) {
logger.log(Level.INFO, "Log composite: Selected root updated event received; Sending 0-index log request ...");
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
sendLogFolderRequest(0);
}
});
}
private void sendLogFolderRequest(int startIndex) {
LogOperationOptions logOptions = new LogOperationOptions();
logOptions.setMaxDatabaseVersionCount(LOG_REQUEST_DATABASE_COUNT);
logOptions.setMaxFileHistoryCount(LOG_REQUEST_FILE_COUNT);
logOptions.setStartDatabaseVersionIndex(startIndex);
pendingLogFolderRequest = new LogFolderRequest();
pendingLogFolderRequest.setRoot(historyModel.getSelectedRoot());
pendingLogFolderRequest.setOptions(logOptions);
logger.log(Level.INFO, "Log composite: Sending log request with ID #" + pendingLogFolderRequest.getId() + " ...");
eventBus.post(pendingLogFolderRequest);
}
@Subscribe
public void onLogFolderResponse(final LogFolderResponse logResponse) {
logger.log(Level.INFO, "Log composite: Log response received.");
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
boolean matchingResponse = pendingLogFolderRequest != null
&& pendingLogFolderRequest.getId() == logResponse.getRequestId();
if (matchingResponse) {
updateTabs(pendingLogFolderRequest, logResponse);
mainPanel.showLog();
pendingLogFolderRequest = null;
}
}
});
}
private void updateTabs(LogFolderRequest logRequest, LogFolderResponse logResponse) {
logger.log(Level.INFO, "Log composite: Updating tabs with log folder response.");
// Dispose all existing tabs (if this is the first request)
boolean firstRequest = logRequest.getOptions().getStartDatabaseVersionIndex() == 0;
if (firstRequest) {
resetAndDisposeAll();
}
// Dispose all loading tabs
while (loadingTabComposites.size() > 0) {
loadingTabComposites.remove(0).dispose();
}
// And create new tabs
List<LightweightDatabaseVersion> newDatabaseVersions = logResponse.getResult().getDatabaseVersions();
int databaseVersionIndex = logRequest.getOptions().getStartDatabaseVersionIndex();
for (LightweightDatabaseVersion databaseVersion : newDatabaseVersions) {
if (databaseVersion.getChangeSet().hasChanges()) {
LogTabComposite tabComposite = new LogTabComposite(this, logContentComposite, historyModel.getSelectedRoot(),
databaseVersionIndex, databaseVersion);
tabComposites.put(databaseVersion.getDate(), tabComposite);
}
databaseVersionIndex++;
}
// Add 'Loading ...' panel (if potentially more databases there)
if (newDatabaseVersions.size() == LOG_REQUEST_DATABASE_COUNT) {
createLoadingComposite();
}
// Highlight
highlightByDate(historyModel.getSelectedDate());
// Then redraw!
redrawAll();
}
@Subscribe
public void onModelSelectedDateUpdatedEvent(final ModelSelectedDateUpdatedEvent event) {
logger.log(Level.INFO, "Log composite: Selected date event received, highlighing tab.");
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
highlightByDate(event.getSelectedDate());
}
});
}
private void createLoadingComposite() {
GridLayout loadingCompositeGridLayout = new GridLayout(1, false);
loadingCompositeGridLayout.marginTop = 0;
loadingCompositeGridLayout.marginLeft = 0;
loadingCompositeGridLayout.marginRight = 0;
loadingCompositeGridLayout.marginBottom = 0;
loadingCompositeGridLayout.horizontalSpacing = 0;
loadingCompositeGridLayout.verticalSpacing = 0;
Composite loadingComposite = new Composite(logContentComposite, SWT.BORDER);
loadingComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 1, 1));
loadingComposite.setLayout(loadingCompositeGridLayout);
loadingComposite.setBackground(WidgetDecorator.WHITE);
loadingComposite.setBackgroundMode(SWT.INHERIT_FORCE);
Label loadMoreLabel = new Label(loadingComposite, SWT.CENTER);
loadMoreLabel.setText(I18n.getText("org.syncany.gui.history.LogComposite.loading"));
loadingComposite.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
if (pendingLogFolderRequest == null) {
int newStartDatabaseIndex = tabComposites.size();
sendLogFolderRequest(newStartDatabaseIndex);
}
}
});
loadingTabComposites.add(loadingComposite);
}
public void onSelectDatabaseVersion(LightweightDatabaseVersion databaseVersion) {
historyModel.setSelectedDate(databaseVersion.getDate());
}
public void onDoubleClickDatabaseVersion(LightweightDatabaseVersion databaseVersion) {
mainPanel.showTree();
}
public void onFileJumpToTree(LightweightDatabaseVersion databaseVersion, String relativeFilePath) {
historyModel.setSelectedDate(databaseVersion.getDate());
historyModel.setSelectedFilePath(relativeFilePath);
mainPanel.showTree();
}
public void onFileOpen(LightweightDatabaseVersion databaseVersion, String relativeFilePath) {
final File file = new File(historyModel.getSelectedRoot(), relativeFilePath);
launchOrDisplayError(file);
}
public void onFileOpenContainingFolder(LightweightDatabaseVersion databaseVersion, String relativeFilePath) {
final File file = new File(historyModel.getSelectedRoot(), relativeFilePath);
launchOrDisplayError(file.getParentFile());
}
public void onFileCopytoClipboard(LightweightDatabaseVersion databaseVersion, String relativeFilePath) {
final File file = new File(historyModel.getSelectedRoot(), relativeFilePath);
DesktopUtil.copyToClipboard(file.getAbsolutePath());
}
private void launchOrDisplayError(File file) {
if (file.exists()) {
DesktopUtil.launch(file.getAbsolutePath());
}
else {
MessageBox messageBox = new MessageBox(mainPanel.getShell(), SWT.ICON_WARNING | SWT.OK);
messageBox.setText(I18n.getText("org.syncany.gui.history.LogTabComposite.warningNotExist.title"));
messageBox.setMessage(I18n.getText("org.syncany.gui.history.LogTabComposite.warningNotExist.description", file.getAbsolutePath()));
messageBox.open();
}
}
public void dispose() {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
eventBus.unregister(LogComposite.this);
for (LogTabComposite tabComposite : tabComposites.values()) {
tabComposite.dispose();
}
}
});
super.dispose();
}
}