/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.gui.history;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Cursor;
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.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.ocpsoft.prettytime.PrettyTime;
import org.syncany.config.GuiEventBus;
import org.syncany.gui.util.I18n;
import org.syncany.gui.util.SWTResourceManager;
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 com.google.common.eventbus.Subscribe;
/**
* The log tag composite is a visual representation of a single database
* version and its changed file versions. Each tab lives inside a {@link LogComposite}
* and displays multiple file entries (added/changed/removed files).
*
* <p>The initial tab is created by the results of the {@link LogFolderResponse} received
* by the {@link LogComposite}. If a certain number of entries is displayed, a "More ..."
* link is shown that lets the user load more entries. This triggers a new {@link LogFolderRequest}
* by this composite. Once the response is received, it composite updates itself.
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class LogTabComposite extends Composite {
private static final Logger logger = Logger.getLogger(LogTabComposite.class.getSimpleName());
private static final String IMAGE_RESOURCE_FORMAT = "/" + HistoryDialog.class.getPackage().getName().replace('.', '/') + "/%s.png";
private static final int LOG_REQUEST_FILE_COUNT_STEP = 25;
private LogComposite logComposite;
private String root;
private int databaseVersionIndex;
private LightweightDatabaseVersion databaseVersion;
private boolean highlighted;
private boolean mouseOver;
private int showMoreLabelEntryLimit;
private GuiEventBus eventBus;
private LogFolderRequest pendingLogFolderRequest;
public LogTabComposite(LogComposite logComposite, Composite logMainComposite, String root, int databaseVersionIndex, LightweightDatabaseVersion databaseVersion) {
super(logMainComposite, SWT.BORDER);
this.logComposite = logComposite;
this.root = root;
this.databaseVersionIndex = databaseVersionIndex;
this.databaseVersion = databaseVersion;
this.highlighted = false;
this.mouseOver = false;
this.showMoreLabelEntryLimit = LogComposite.LOG_REQUEST_FILE_COUNT;
this.eventBus = null; // Register on demand!
this.pendingLogFolderRequest = null;
this.createControls();
this.addMouseListeners();
}
private void createControls() {
createMainComposite();
createDateLabels();
createEntryLabels();
}
private void createMainComposite() {
GridLayout mainCompositeGridLayout = new GridLayout(3, false);
mainCompositeGridLayout.marginTop = 0;
mainCompositeGridLayout.marginLeft = 0;
mainCompositeGridLayout.marginRight = 0;
mainCompositeGridLayout.marginBottom = 0;
mainCompositeGridLayout.horizontalSpacing = 0;
mainCompositeGridLayout.verticalSpacing = 0;
setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));
setLayout(mainCompositeGridLayout);
setBackground(WidgetDecorator.WHITE);
setBackgroundMode(SWT.INHERIT_FORCE);
}
private void createDateLabels() {
Label dateLabel = new Label(this, SWT.NONE);
dateLabel.setText(databaseVersion.getDate().toString());
dateLabel.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
WidgetDecorator.bold(dateLabel);
GridData prettyDateLabelGriData = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
prettyDateLabelGriData.horizontalIndent = 10;
Label prettyDateLabel = new Label(this, SWT.NONE);
prettyDateLabel.setLayoutData(prettyDateLabelGriData);
prettyDateLabel.setText(new PrettyTime().format(databaseVersion.getDate()));
prettyDateLabel.setForeground(WidgetDecorator.DARK_GRAY);
WidgetDecorator.smaller(prettyDateLabel);
}
private void createEntryLabels() {
Label spacingLabel1 = new Label(this, SWT.NONE);
spacingLabel1.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 3, 1));
for (String file : databaseVersion.getChangeSet().getNewFiles()) {
createFileEntryLabel(file, "add", true);
}
for (String file : databaseVersion.getChangeSet().getChangedFiles()) {
createFileEntryLabel(file, "edit", true);
}
for (String file : databaseVersion.getChangeSet().getDeletedFiles()) {
createFileEntryLabel(file, "delete", false);
}
// Add 'more ...' entry if max. number reached
int totalEntryCount = databaseVersion.getChangeSet().getNewFiles().size()
+ databaseVersion.getChangeSet().getChangedFiles().size()
+ databaseVersion.getChangeSet().getDeletedFiles().size();
if (totalEntryCount == showMoreLabelEntryLimit) {
createMoreEntryLabel();
}
Label spacingLabel2 = new Label(this, SWT.NONE);
spacingLabel2.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 3, 1));
}
private void createFileEntryLabel(String relativeFilePath, String imageResourceName, boolean link) {
Label fileLabel = createEntryLabel(relativeFilePath, imageResourceName);
if (link) {
fileLabel.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_HAND));
addFileEntryMenu(fileLabel, relativeFilePath);
addFileEntryMouseClickListener(fileLabel, relativeFilePath);
addEntryMouseMoveListener(fileLabel);
}
}
private void createMoreEntryLabel() {
Label moreLabel = createEntryLabel(I18n.getText("org.syncany.gui.history.LogTabComposite.more"), "more");
moreLabel.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_HAND));
addEntryMouseMoveListener(moreLabel);
addMoreEntryClickListener(moreLabel);
}
private Label createEntryLabel(String relativeFilePath, String imageResourceName) {
GridData imageLabelGridData = new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1);
imageLabelGridData.horizontalIndent = 20;
imageLabelGridData.verticalIndent = 2;
Label imageLabel = new Label(this, SWT.NONE);
imageLabel.setLayoutData(imageLabelGridData);
imageLabel.setImage(SWTResourceManager.getImage(String.format(IMAGE_RESOURCE_FORMAT, imageResourceName)));
GridData entryLabelGridData = new GridData(SWT.LEFT, SWT.TOP, true, false, 2, 1);
entryLabelGridData.horizontalIndent = 10;
entryLabelGridData.verticalIndent = 2;
Label entryLabel = new Label(this, SWT.NONE);
entryLabel.setLayoutData(entryLabelGridData);
entryLabel.setText(relativeFilePath.replaceAll("&", "&&"));
return entryLabel;
}
private void addFileEntryMenu(Label fileLabel, final String relativeFilePath) {
final Menu fileMenu = new Menu(fileLabel);
MenuItem jumpToTreeMenuItem = new MenuItem(fileMenu, SWT.NONE);
jumpToTreeMenuItem.setText(I18n.getText("org.syncany.gui.history.LogTabComposite.jumpToTree"));
jumpToTreeMenuItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
logComposite.onFileJumpToTree(databaseVersion, relativeFilePath);
}
});
new MenuItem(fileMenu, SWT.SEPARATOR);
MenuItem openFileMenuItem = new MenuItem(fileMenu, SWT.NONE);
openFileMenuItem.setText(I18n.getText("org.syncany.gui.history.LogTabComposite.openFile"));
openFileMenuItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
logComposite.onFileOpen(databaseVersion, relativeFilePath);
}
});
MenuItem openFolderMenuItem = new MenuItem(fileMenu, SWT.NONE);
openFolderMenuItem.setText(I18n.getText("org.syncany.gui.history.LogTabComposite.openContainingFolder"));
openFolderMenuItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
logComposite.onFileOpenContainingFolder(databaseVersion, relativeFilePath);
}
});
new MenuItem(fileMenu, SWT.SEPARATOR);
MenuItem copyPathMenuItem = new MenuItem(fileMenu, SWT.NONE);
copyPathMenuItem.setText(I18n.getText("org.syncany.gui.history.LogTabComposite.copyPath"));
copyPathMenuItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
logComposite.onFileCopytoClipboard(databaseVersion, relativeFilePath);
}
});
fileMenu.setDefaultItem(jumpToTreeMenuItem);
fileLabel.setMenu(fileMenu);
}
private void addMoreEntryClickListener(Label moreLabel) {
moreLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
logger.log(Level.INFO, "Reloading database version.");
sendMoreLogFolderRequest();
}
});
}
private void addFileEntryMouseClickListener(final Label fileLabel, final String relativeFilePath) {
fileLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
logComposite.onFileJumpToTree(databaseVersion, relativeFilePath);
}
});
}
private void addEntryMouseMoveListener(final Label fileLabel) {
fileLabel.addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
fileLabel.setForeground(WidgetDecorator.BLUE_LINK);
}
@Override
public void mouseExit(MouseEvent e) {
fileLabel.setForeground(WidgetDecorator.BLACK);
}
});
}
/**
* Highlights the tab or removes the highlight color
* from the tab.
*/
public void setHighlighted(boolean highlighted) {
this.highlighted = highlighted;
updateHighlighted();
}
private void updateHighlighted() {
if (mouseOver || highlighted) {
if (getBackground() != WidgetDecorator.LIGHT_GRAY) {
setBackground(WidgetDecorator.LIGHT_GRAY);
}
}
else {
if (getBackground() != WidgetDecorator.WHITE) {
setBackground(WidgetDecorator.WHITE);
}
}
}
private void addMouseListeners() {
addMouseListeners(this);
for (Control control : getChildren()) {
addMouseListeners(control);
}
}
private void addMouseListeners(Control control) {
control.addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
mouseOver = true;
updateHighlighted();
}
@Override
public void mouseExit(MouseEvent e) {
mouseOver = false;
updateHighlighted();
}
});
control.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
logComposite.onSelectDatabaseVersion(databaseVersion);
}
@Override
public void mouseDoubleClick(MouseEvent e) {
logComposite.onDoubleClickDatabaseVersion(databaseVersion);
}
});
control.addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseScrolled(MouseEvent e) {
// Pass on mouse events to parent control.
logComposite.scrollBy(e.count);
}
});
}
private void sendMoreLogFolderRequest() {
showMoreLabelEntryLimit += LOG_REQUEST_FILE_COUNT_STEP;
LogOperationOptions logOptions = new LogOperationOptions();
logOptions.setMaxDatabaseVersionCount(1);
logOptions.setStartDatabaseVersionIndex(databaseVersionIndex);
logOptions.setMaxFileHistoryCount(showMoreLabelEntryLimit);
pendingLogFolderRequest = new LogFolderRequest();
pendingLogFolderRequest.setRoot(root);
pendingLogFolderRequest.setOptions(logOptions);
if (eventBus == null) {
eventBus = GuiEventBus.getAndRegister(this);
}
eventBus.post(pendingLogFolderRequest);
}
@Subscribe
public void onLogFolderResponse(final LogFolderResponse logResponse) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
if (pendingLogFolderRequest != null && pendingLogFolderRequest.getId() == logResponse.getRequestId()) {
databaseVersion = logResponse.getResult().getDatabaseVersions().get(0);
disposeControls();
createControls();
redrawParents();
pendingLogFolderRequest = null;
}
}
});
}
private void disposeControls() {
for (Control control : getChildren()) {
control.dispose();
}
}
private void redrawParents() {
LogTabComposite.this.layout();
logComposite.redrawAll();
}
@Override
public void dispose() {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
if (eventBus != null) {
eventBus.unregister(LogTabComposite.this);
}
}
});
super.dispose();
}
}