Minor cleanup for workspace size tracking code.
[simantics/platform.git] / bundles / org.simantics.ui.workspace.tracker / src / org / simantics / ui / workspace / tracker / internal / contributions / WorkspaceSizeTrackerTrim.java
1 /*******************************************************************************
2  * Copyright (c) 2017 Association for Decentralized Information Management in
3  * Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     Semantum Oy - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.ui.workspace.tracker.internal.contributions;
13
14 import java.io.IOException;
15 import java.nio.file.FileStore;
16 import java.nio.file.Files;
17 import java.nio.file.Path;
18
19 import org.eclipse.e4.ui.model.application.ui.menu.MToolControl;
20 import org.eclipse.jface.action.Action;
21 import org.eclipse.jface.action.IMenuManager;
22 import org.eclipse.jface.action.MenuManager;
23 import org.eclipse.jface.preference.IPreferenceStore;
24 import org.eclipse.jface.util.IPropertyChangeListener;
25 import org.eclipse.osgi.util.NLS;
26 import org.eclipse.swt.SWT;
27 import org.eclipse.swt.graphics.Color;
28 import org.eclipse.swt.graphics.GC;
29 import org.eclipse.swt.graphics.Point;
30 import org.eclipse.swt.graphics.Rectangle;
31 import org.eclipse.swt.widgets.Composite;
32 import org.eclipse.swt.widgets.Control;
33 import org.eclipse.swt.widgets.Display;
34 import org.eclipse.swt.widgets.Listener;
35 import org.eclipse.swt.widgets.Menu;
36 import org.eclipse.swt.widgets.Shell;
37 import org.simantics.filesystem.services.sizetracker.SizeTracker;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * The Heap Status control, which shows the heap usage statistics in the window trim.
43  *
44  * @since 3.1
45  */
46 public class WorkspaceSizeTrackerTrim extends Composite {
47
48         private Logger logger = LoggerFactory.getLogger(WorkspaceSizeTrackerTrim.class);
49
50         private MToolControl toolControl;
51         private SizeTracker sizeTracker;
52         private IPreferenceStore prefStore;
53
54         /**
55          * How many MB of free disk space means we are low on disk space? 
56          */
57         private long lowSpaceThreshold = IWorkspaceSizeTrackerConstants.DEFAULT_LOW_SPACE_THRESHOLD;
58         private boolean highlightLowSpace = IWorkspaceSizeTrackerConstants.DEFAULT_HIGHLIGHT_LOW_SPACE;
59         private int updateInterval = IWorkspaceSizeTrackerConstants.DEFAULT_UPDATE_INTERVAL;
60
61         private Color bgCol, usedSpaceCol, lowSpaceCol, topLeftCol, bottomRightCol, sepCol, textCol;
62         @SuppressWarnings("unused")
63         private Color markCol;
64
65         private String storeName;
66         private long totalSpace;
67         private long availableSpace;
68         private long usedSpace;
69         private long prevTotalSpace = -1L;
70         private long prevAvailableSpace = -1L;
71         private long prevUsedSpace = -1L;
72
73         private boolean hasChanged;
74         private long mark = -1;
75
76         private boolean updateTooltip = false;
77
78         private final Runnable timer = new Runnable() {
79                 @Override
80                 public void run() {
81                         if (!isDisposed()) {
82                                 safeUpdateStats();
83                                 if (hasChanged) {
84                                         if (updateTooltip) {
85                                                 updateToolTip();
86                                         }
87                                         redraw();
88                                         hasChanged = false;
89                                 }
90                                 getDisplay().timerExec(updateInterval, this);
91                         }
92                 }
93         };
94
95         private final IPropertyChangeListener prefListener = event -> {
96                 if (IWorkspaceSizeTrackerConstants.PREF_UPDATE_INTERVAL.equals(event.getProperty())) {
97                         setUpdateIntervalInMS(prefStore.getInt(IWorkspaceSizeTrackerConstants.PREF_UPDATE_INTERVAL));
98                 } else if (IWorkspaceSizeTrackerConstants.PREF_HIGHLIGHT_LOW_SPACE.equals(event.getProperty())) {
99                         highlightLowSpace = prefStore.getBoolean(IWorkspaceSizeTrackerConstants.PREF_HIGHLIGHT_LOW_SPACE);
100                         hasChanged = true;
101                 } else if (IWorkspaceSizeTrackerConstants.PREF_LOW_SPACE_THRESHOLD.equals(event.getProperty())) {
102                         lowSpaceThreshold = prefStore.getLong(IWorkspaceSizeTrackerConstants.PREF_LOW_SPACE_THRESHOLD);
103                         hasChanged = true;
104                 } else if (IWorkspaceSizeTrackerConstants.PREF_SHOW_MONITOR.equals(event.getProperty())) {
105                         boolean show = prefStore.getBoolean(IWorkspaceSizeTrackerConstants.PREF_SHOW_MONITOR);
106                         if (!show)
107                                 showTracker(false);
108                 }
109         };
110
111         /**
112          * Creates a new heap status control with the given parent, and using
113          * the given preference store to obtain settings such as the refresh
114          * interval.
115          * @param toolControl 
116          *
117          * @param parent the parent composite
118          * @param sizeTracker the workspace sizeTracker service 
119          * @param prefStore the preference store
120          */
121         public WorkspaceSizeTrackerTrim(Composite parent, MToolControl toolControl, SizeTracker sizeTracker, IPreferenceStore prefStore) {
122                 super(parent, SWT.NONE);
123                 this.toolControl = toolControl;
124                 this.sizeTracker = sizeTracker;
125
126                 this.prefStore = prefStore;
127                 prefStore.addPropertyChangeListener(prefListener);
128
129                 setUpdateIntervalInMS(prefStore.getInt(IWorkspaceSizeTrackerConstants.PREF_UPDATE_INTERVAL));
130                 highlightLowSpace = prefStore.getBoolean(IWorkspaceSizeTrackerConstants.PREF_HIGHLIGHT_LOW_SPACE);
131                 lowSpaceThreshold = prefStore.getLong(IWorkspaceSizeTrackerConstants.PREF_LOW_SPACE_THRESHOLD);
132
133                 Display display = getDisplay();
134                 usedSpaceCol = display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW);
135                 lowSpaceCol = new Color(display, 255, 70, 70);  // medium red
136                 bgCol = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
137                 sepCol = topLeftCol = display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW);
138                 bottomRightCol = display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW);
139                 markCol = textCol = display.getSystemColor(SWT.COLOR_WIDGET_FOREGROUND);
140
141                 createContextMenu();
142
143                 Listener listener = event -> {
144                         switch (event.type) {
145                         case SWT.Dispose:
146                                 doDispose();
147                                 break;
148                         case SWT.Paint:
149                                 if (event.widget == WorkspaceSizeTrackerTrim.this) {
150                                         paintComposite(event.gc);
151                                 }
152                                 break;
153                         case SWT.MouseDown:
154                                 if (event.button == 1) {
155                                         if (event.widget == WorkspaceSizeTrackerTrim.this) {
156                                                 setMark();
157                                         }
158                                 }
159                                 break;
160                         case SWT.MouseEnter:
161                                 WorkspaceSizeTrackerTrim.this.updateTooltip = true;
162                                 updateToolTip();
163                                 break;
164                         case SWT.MouseExit:
165                                 if (event.widget == WorkspaceSizeTrackerTrim.this) {
166                                         WorkspaceSizeTrackerTrim.this.updateTooltip = false;
167                                 }
168                                 break;
169                         }
170                 };
171                 addListener(SWT.Dispose, listener);
172                 addListener(SWT.MouseDown, listener);
173                 addListener(SWT.Paint, listener);
174                 addListener(SWT.MouseEnter, listener);
175                 addListener(SWT.MouseExit, listener);
176
177                 // make sure stats are updated before first paint
178                 safeUpdateStats();
179
180                 getDisplay().asyncExec(() -> {
181                         if (!isDisposed()) {
182                                 getDisplay().timerExec(updateInterval, timer);
183                         }
184                 });
185         }
186
187         @Override
188         public void setBackground(Color color) {
189                 bgCol = color;
190         }
191
192         @Override
193         public void setForeground(Color color) {
194                 if (color == null) {
195                         markCol = textCol = getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND);
196                 } else {
197                         markCol = textCol = color;
198                 }
199         }
200
201         @Override
202         public Color getForeground() {
203                 if (usedSpaceCol != null) {
204                         return usedSpaceCol;
205                 }
206                 return getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND);
207         }
208
209         private void setUpdateIntervalInMS(int interval) {
210                 updateInterval = Math.max(100, interval);
211         }
212
213         private void doDispose() {
214                 prefStore.removePropertyChangeListener(prefListener);
215                 if (lowSpaceCol != null) {
216                         lowSpaceCol.dispose();
217                 }
218         }
219
220         @Override
221         public Point computeSize(int wHint, int hHint, boolean changed) {
222                 GC gc = new GC(this);
223                 Point p = gc.textExtent(Messages.WorkspaceSizeTrackerTrim_widthStr);
224                 int height = p.y + 4;
225                 gc.dispose();
226                 return new Point(p.x, height);
227         }
228
229         /**
230          * Creates the context menu
231          */
232         private void createContextMenu() {
233                 MenuManager menuMgr = new MenuManager();
234                 menuMgr.setRemoveAllWhenShown(true);
235                 menuMgr.addMenuListener(menuMgr1 -> fillMenu(menuMgr1));
236                 Menu menu = menuMgr.createContextMenu(this);
237                 setMenu(menu);
238         }
239
240         private void fillMenu(IMenuManager menuMgr) {
241                 menuMgr.add(new SetMarkAction());
242                 menuMgr.add(new ClearMarkAction());
243                 menuMgr.add(new CloseSizeTrackerTrimAction());
244         }
245
246         /**
247          * Sets the mark to the current usedMem level.
248          */
249         private void setMark() {
250                 safeUpdateStats();  // get up-to-date stats before taking the mark
251                 mark = usedSpace;
252                 hasChanged = true;
253                 redraw();
254         }
255
256         /**
257          * Clears the mark.
258          */
259         private void clearMark() {
260                 mark = -1;
261                 hasChanged = true;
262                 redraw();
263         }
264
265         private void paintComposite(GC gc) {
266                 paintCompositeMaxUnknown(gc);
267         }
268
269         private void paintCompositeMaxUnknown(GC gc) {
270                 Rectangle rect = getClientArea();
271                 int x = rect.x;
272                 int y = rect.y;
273                 int w = rect.width;
274                 int h = rect.height;
275                 int sw = w - 1; // status width
276                 long storeUsedSpace = totalSpace - availableSpace;
277                 int uw = (int) (sw * storeUsedSpace / totalSpace); // used space width
278                 int ux = x + 1 + uw; // used space right edge
279                 if (bgCol != null) {
280                         gc.setBackground(bgCol);
281                 }
282                 gc.fillRectangle(rect);
283
284                 boolean lowOnSpace = false;
285                 if (highlightLowSpace) {
286                         lowOnSpace = (1024L*1024L*lowSpaceThreshold) >= availableSpace;
287                 }
288
289                 gc.setForeground(sepCol);
290                 gc.drawLine(ux, y, ux, y + h);
291                 gc.setForeground(topLeftCol);
292                 gc.drawLine(x, y, x+w, y);
293                 gc.drawLine(x, y, x, y+h);
294                 gc.setForeground(bottomRightCol);
295                 gc.drawLine(x+w-1, y, x+w-1, y+h);
296                 gc.drawLine(x, y+h-1, x+w, y+h-1);
297
298                 gc.setBackground(lowOnSpace ? lowSpaceCol : usedSpaceCol);
299                 gc.fillRectangle(x + 1, y + 1, uw, h - 2);
300
301                 String s = NLS.bind(Messages.WorkspaceSizeTrackerTrim_status, convertToSizeString(usedSpace), convertToSizeString(availableSpace));
302                 Point p = gc.textExtent(s);
303                 int sx = (rect.width - 15 - p.x) / 2 + rect.x + 1;
304                 int sy = (rect.height - 2 - p.y) / 2 + rect.y + 1;
305                 gc.setForeground(textCol);
306                 gc.drawString(s, sx, sy, true);
307         }
308
309         private void safeUpdateStats()  {
310                 try {
311                         updateStats();
312                 } catch (IOException e) {
313                         logger.error("Failed to update workspace size statistics.", e);
314                 }
315         }
316
317         private void updateStats() throws IOException {
318                 Path path = sizeTracker.path();
319                 FileStore store = Files.getFileStore(path);
320
321                 storeName = store.toString();
322                 totalSpace = store.getTotalSpace();
323                 availableSpace = store.getUsableSpace();
324                 usedSpace = sizeTracker.size();
325
326                 if (convertToMeg(prevTotalSpace) != convertToMeg(totalSpace)) {
327                         prevTotalSpace = totalSpace;
328                         this.hasChanged = true;
329                 }
330                 if (prevAvailableSpace != availableSpace) {
331                         prevAvailableSpace = availableSpace;
332                         this.hasChanged = true;
333                 }
334                 if (convertToMeg(prevUsedSpace) != convertToMeg(usedSpace)) {
335                         prevUsedSpace = usedSpace;
336                         this.hasChanged = true;
337                 }
338         }
339
340         private void updateToolTip() {
341                 String usedStr = convertToSizeString(usedSpace);
342                 String availableStr = convertToSizeString(availableSpace);
343                 String totalStr = convertToSizeString(totalSpace);
344                 String markStr = mark == -1 ? Messages.WorkspaceSizeTrackerTrim_noMark : convertToSizeString(mark);
345                 String toolTip = NLS.bind(Messages.WorkspaceSizeTrackerTrim_memoryToolTip, new Object[] { usedStr, storeName, availableStr, totalStr, markStr });
346                 if (!toolTip.equals(getToolTipText())) {
347                         setToolTipText(toolTip);
348                 }
349         }
350
351         /**
352          * Converts the given number of bytes to a printable number of megabytes (rounded up).
353          */
354         private String convertToSizeString(long numBytes) {
355                 long megs = convertToMeg(numBytes);
356                 if (megs > 10000) {
357                         double megsd = (double) megs;
358                         long gigs = (long) Math.floor(megsd / 1024.0);
359                         long decimals = (long) (megsd - gigs*1024);
360                         decimals = (decimals + 5) / 10;
361                         return NLS.bind(Messages.WorkspaceSizeTrackerTrim_gig, new Long(gigs), new Long(decimals));
362                 } else {
363                         return NLS.bind(Messages.WorkspaceSizeTrackerTrim_meg, new Long(megs));
364                 }
365         }
366
367         /**
368          * Converts the given number of bytes to the corresponding number of megabytes (rounded up).
369          */
370         private long convertToMeg(long numBytes) {
371                 return (numBytes + (512 * 1024)) / (1024 * 1024);
372         }
373
374         class SetMarkAction extends Action {
375                 SetMarkAction() {
376                         super(Messages.SetMarkAction_text);
377                 }
378
379                 @Override
380                 public void run() {
381                         setMark();
382                 }
383         }
384
385         class ClearMarkAction extends Action {
386                 ClearMarkAction() {
387                         super(Messages.ClearMarkAction_text);
388                 }
389
390                 @Override
391                 public void run() {
392                         clearMark();
393                 }
394         }
395
396         class CloseSizeTrackerTrimAction extends Action{
397                 CloseSizeTrackerTrimAction(){
398                         super(Messages.WorkspaceSizeTrackerTrim_close);
399                 }
400
401                 @Override
402                 public void run(){
403                         showTracker(false);
404                 }
405         }
406
407         private void showTracker(boolean show) {
408                 if (toolControl.isToBeRendered() != show) {
409                         Object widget = toolControl.getWidget();
410                         Shell shell = widget instanceof Control ? ((Control) widget).getShell() : null;
411                         toolControl.setToBeRendered(show);
412                         if (shell != null)
413                                 shell.layout(null, SWT.ALL | SWT.CHANGED | SWT.DEFER);
414                         prefStore.setValue(IWorkspaceSizeTrackerConstants.PREF_SHOW_MONITOR, show);
415                 }
416         }
417
418 }