/******************************************************************************* * Copyright (c) 2017 Association for Decentralized Information Management in * Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Semantum Oy - initial API and implementation *******************************************************************************/ package org.simantics.ui.workspace.tracker.internal.contributions; import java.io.IOException; import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.Path; import org.eclipse.e4.ui.model.application.ui.menu.MToolControl; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import org.simantics.filesystem.services.sizetracker.SizeTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The Heap Status control, which shows the heap usage statistics in the window trim. * * @since 3.1 */ public class WorkspaceSizeTrackerTrim extends Composite { private Logger logger = LoggerFactory.getLogger(WorkspaceSizeTrackerTrim.class); private MToolControl toolControl; private SizeTracker sizeTracker; private IPreferenceStore prefStore; /** * How many MB of free disk space means we are low on disk space? */ private long lowSpaceThreshold = IWorkspaceSizeTrackerConstants.DEFAULT_LOW_SPACE_THRESHOLD; private boolean highlightLowSpace = IWorkspaceSizeTrackerConstants.DEFAULT_HIGHLIGHT_LOW_SPACE; private int updateInterval = IWorkspaceSizeTrackerConstants.DEFAULT_UPDATE_INTERVAL; private Color bgCol, usedSpaceCol, lowSpaceCol, topLeftCol, bottomRightCol, sepCol, textCol; @SuppressWarnings("unused") private Color markCol; private String storeName; private long totalSpace; private long availableSpace; private long usedSpace; private long prevTotalSpace = -1L; private long prevAvailableSpace = -1L; private long prevUsedSpace = -1L; private boolean hasChanged; private long mark = -1; private boolean updateTooltip = false; private final Runnable timer = new Runnable() { @Override public void run() { if (!isDisposed()) { safeUpdateStats(); if (hasChanged) { if (updateTooltip) { updateToolTip(); } redraw(); hasChanged = false; } getDisplay().timerExec(updateInterval, this); } } }; private final IPropertyChangeListener prefListener = event -> { if (IWorkspaceSizeTrackerConstants.PREF_UPDATE_INTERVAL.equals(event.getProperty())) { setUpdateIntervalInMS(prefStore.getInt(IWorkspaceSizeTrackerConstants.PREF_UPDATE_INTERVAL)); } else if (IWorkspaceSizeTrackerConstants.PREF_HIGHLIGHT_LOW_SPACE.equals(event.getProperty())) { highlightLowSpace = prefStore.getBoolean(IWorkspaceSizeTrackerConstants.PREF_HIGHLIGHT_LOW_SPACE); hasChanged = true; } else if (IWorkspaceSizeTrackerConstants.PREF_LOW_SPACE_THRESHOLD.equals(event.getProperty())) { lowSpaceThreshold = prefStore.getLong(IWorkspaceSizeTrackerConstants.PREF_LOW_SPACE_THRESHOLD); hasChanged = true; } else if (IWorkspaceSizeTrackerConstants.PREF_SHOW_MONITOR.equals(event.getProperty())) { boolean show = prefStore.getBoolean(IWorkspaceSizeTrackerConstants.PREF_SHOW_MONITOR); if (!show) showTracker(false); } }; /** * Creates a new heap status control with the given parent, and using * the given preference store to obtain settings such as the refresh * interval. * @param toolControl * * @param parent the parent composite * @param sizeTracker the workspace sizeTracker service * @param prefStore the preference store */ public WorkspaceSizeTrackerTrim(Composite parent, MToolControl toolControl, SizeTracker sizeTracker, IPreferenceStore prefStore) { super(parent, SWT.NONE); this.toolControl = toolControl; this.sizeTracker = sizeTracker; this.prefStore = prefStore; prefStore.addPropertyChangeListener(prefListener); setUpdateIntervalInMS(prefStore.getInt(IWorkspaceSizeTrackerConstants.PREF_UPDATE_INTERVAL)); highlightLowSpace = prefStore.getBoolean(IWorkspaceSizeTrackerConstants.PREF_HIGHLIGHT_LOW_SPACE); lowSpaceThreshold = prefStore.getLong(IWorkspaceSizeTrackerConstants.PREF_LOW_SPACE_THRESHOLD); Display display = getDisplay(); usedSpaceCol = display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW); lowSpaceCol = new Color(display, 255, 70, 70); // medium red bgCol = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); sepCol = topLeftCol = display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW); bottomRightCol = display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW); markCol = textCol = display.getSystemColor(SWT.COLOR_WIDGET_FOREGROUND); createContextMenu(); Listener listener = event -> { switch (event.type) { case SWT.Dispose: doDispose(); break; case SWT.Paint: if (event.widget == WorkspaceSizeTrackerTrim.this) { paintComposite(event.gc); } break; case SWT.MouseDown: if (event.button == 1) { if (event.widget == WorkspaceSizeTrackerTrim.this) { setMark(); } } break; case SWT.MouseEnter: WorkspaceSizeTrackerTrim.this.updateTooltip = true; updateToolTip(); break; case SWT.MouseExit: if (event.widget == WorkspaceSizeTrackerTrim.this) { WorkspaceSizeTrackerTrim.this.updateTooltip = false; } break; } }; addListener(SWT.Dispose, listener); addListener(SWT.MouseDown, listener); addListener(SWT.Paint, listener); addListener(SWT.MouseEnter, listener); addListener(SWT.MouseExit, listener); // make sure stats are updated before first paint safeUpdateStats(); getDisplay().asyncExec(() -> { if (!isDisposed()) { getDisplay().timerExec(updateInterval, timer); } }); } @Override public void setBackground(Color color) { bgCol = color; } @Override public void setForeground(Color color) { if (color == null) { markCol = textCol = getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND); } else { markCol = textCol = color; } } @Override public Color getForeground() { if (usedSpaceCol != null) { return usedSpaceCol; } return getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND); } private void setUpdateIntervalInMS(int interval) { updateInterval = Math.max(100, interval); } private void doDispose() { prefStore.removePropertyChangeListener(prefListener); if (lowSpaceCol != null) { lowSpaceCol.dispose(); } } @Override public Point computeSize(int wHint, int hHint, boolean changed) { GC gc = new GC(this); Point p = gc.textExtent(Messages.WorkspaceSizeTrackerTrim_widthStr); int height = p.y + 4; gc.dispose(); return new Point(p.x, height); } /** * Creates the context menu */ private void createContextMenu() { MenuManager menuMgr = new MenuManager(); menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(menuMgr1 -> fillMenu(menuMgr1)); Menu menu = menuMgr.createContextMenu(this); setMenu(menu); } private void fillMenu(IMenuManager menuMgr) { menuMgr.add(new SetMarkAction()); menuMgr.add(new ClearMarkAction()); menuMgr.add(new CloseSizeTrackerTrimAction()); } /** * Sets the mark to the current usedMem level. */ private void setMark() { safeUpdateStats(); // get up-to-date stats before taking the mark mark = usedSpace; hasChanged = true; redraw(); } /** * Clears the mark. */ private void clearMark() { mark = -1; hasChanged = true; redraw(); } private void paintComposite(GC gc) { paintCompositeMaxUnknown(gc); } private void paintCompositeMaxUnknown(GC gc) { Rectangle rect = getClientArea(); int x = rect.x; int y = rect.y; int w = rect.width; int h = rect.height; int sw = w - 1; // status width long storeUsedSpace = totalSpace - availableSpace; int uw = (int) (sw * storeUsedSpace / totalSpace); // used space width int ux = x + 1 + uw; // used space right edge if (bgCol != null) { gc.setBackground(bgCol); } gc.fillRectangle(rect); boolean lowOnSpace = false; if (highlightLowSpace) { lowOnSpace = (1024L*1024L*lowSpaceThreshold) >= availableSpace; } gc.setForeground(sepCol); gc.drawLine(ux, y, ux, y + h); gc.setForeground(topLeftCol); gc.drawLine(x, y, x+w, y); gc.drawLine(x, y, x, y+h); gc.setForeground(bottomRightCol); gc.drawLine(x+w-1, y, x+w-1, y+h); gc.drawLine(x, y+h-1, x+w, y+h-1); gc.setBackground(lowOnSpace ? lowSpaceCol : usedSpaceCol); gc.fillRectangle(x + 1, y + 1, uw, h - 2); String s = NLS.bind(Messages.WorkspaceSizeTrackerTrim_status, convertToSizeString(usedSpace), convertToSizeString(availableSpace)); Point p = gc.textExtent(s); int sx = (rect.width - 15 - p.x) / 2 + rect.x + 1; int sy = (rect.height - 2 - p.y) / 2 + rect.y + 1; gc.setForeground(textCol); gc.drawString(s, sx, sy, true); } private void safeUpdateStats() { try { updateStats(); } catch (IOException e) { logger.error("Failed to update workspace size statistics.", e); } } private void updateStats() throws IOException { Path path = sizeTracker.path(); FileStore store = Files.getFileStore(path); storeName = store.toString(); totalSpace = store.getTotalSpace(); availableSpace = store.getUsableSpace(); usedSpace = sizeTracker.size(); if (convertToMeg(prevTotalSpace) != convertToMeg(totalSpace)) { prevTotalSpace = totalSpace; this.hasChanged = true; } if (prevAvailableSpace != availableSpace) { prevAvailableSpace = availableSpace; this.hasChanged = true; } if (convertToMeg(prevUsedSpace) != convertToMeg(usedSpace)) { prevUsedSpace = usedSpace; this.hasChanged = true; } } private void updateToolTip() { String usedStr = convertToSizeString(usedSpace); String availableStr = convertToSizeString(availableSpace); String totalStr = convertToSizeString(totalSpace); String markStr = mark == -1 ? Messages.WorkspaceSizeTrackerTrim_noMark : convertToSizeString(mark); String toolTip = NLS.bind(Messages.WorkspaceSizeTrackerTrim_memoryToolTip, new Object[] { usedStr, storeName, availableStr, totalStr, markStr }); if (!toolTip.equals(getToolTipText())) { setToolTipText(toolTip); } } /** * Converts the given number of bytes to a printable number of megabytes (rounded up). */ private String convertToSizeString(long numBytes) { long megs = convertToMeg(numBytes); if (megs > 10000) { double megsd = (double) megs; long gigs = (long) Math.floor(megsd / 1024.0); long decimals = (long) (megsd - gigs*1024); decimals = (decimals + 5) / 10; return NLS.bind(Messages.WorkspaceSizeTrackerTrim_gig, new Long(gigs), new Long(decimals)); } else { return NLS.bind(Messages.WorkspaceSizeTrackerTrim_meg, new Long(megs)); } } /** * Converts the given number of bytes to the corresponding number of megabytes (rounded up). */ private long convertToMeg(long numBytes) { return (numBytes + (512 * 1024)) / (1024 * 1024); } class SetMarkAction extends Action { SetMarkAction() { super(Messages.SetMarkAction_text); } @Override public void run() { setMark(); } } class ClearMarkAction extends Action { ClearMarkAction() { super(Messages.ClearMarkAction_text); } @Override public void run() { clearMark(); } } class CloseSizeTrackerTrimAction extends Action{ CloseSizeTrackerTrimAction(){ super(Messages.WorkspaceSizeTrackerTrim_close); } @Override public void run(){ showTracker(false); } } private void showTracker(boolean show) { if (toolControl.isToBeRendered() != show) { Object widget = toolControl.getWidget(); Shell shell = widget instanceof Control ? ((Control) widget).getShell() : null; toolControl.setToBeRendered(show); if (shell != null) shell.layout(null, SWT.ALL | SWT.CHANGED | SWT.DEFER); prefStore.setValue(IWorkspaceSizeTrackerConstants.PREF_SHOW_MONITOR, show); } } }