-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 443a50a
Showing
5 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/out/ | ||
/*.jar |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<idea-plugin version="2"> | ||
<id>com.darkyen.darkyenustimetracker</id> | ||
<name>Darkyenus Time Tracker</name> | ||
<version>1.0</version> | ||
<vendor email="darkyen@me.com" url="http://darkyenus.github.io">Darkyen</vendor> | ||
<category>Miscellaneous</category> | ||
|
||
<description><![CDATA[ | ||
Simple plugin for lightweight tracking of time spent on project.<br> | ||
Adds a single status bar widget: click to start counting, click again to stop. | ||
Pauses the timer automatically when idle (after two minutes of inactivity). | ||
Time is saved in IDE's workspace files, does not clutter project's directory. | ||
<br> | ||
<a href="https://github.com/Darkyenus/DarkyenusTimeTracker">Source available on GitHub</a> | ||
]]></description> | ||
|
||
<change-notes><![CDATA[ | ||
<h3>Version 1.0</h3> | ||
<ul> | ||
<li>Initial release</li> | ||
</ul> | ||
]]> | ||
</change-notes> | ||
|
||
<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html for description --> | ||
<idea-version since-build="145.0"/> | ||
|
||
<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html | ||
on how to target different products --> | ||
<depends>com.intellij.modules.platform</depends> | ||
|
||
<extensions defaultExtensionNs="com.intellij"> | ||
<!-- Add your extensions here --> | ||
</extensions> | ||
|
||
<actions> | ||
<!-- Add your actions here --> | ||
</actions> | ||
|
||
<project-components> | ||
<component> | ||
<implementation-class>com.darkyen.TimeTrackerComponent</implementation-class> | ||
</component> | ||
</project-components> | ||
|
||
</idea-plugin> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package com.darkyen; | ||
|
||
import com.intellij.openapi.components.*; | ||
import com.intellij.openapi.project.Project; | ||
import com.intellij.openapi.wm.WindowManager; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
/** | ||
* | ||
*/ | ||
@State( | ||
name="DarkyenusTimeTracker", | ||
storages = {@Storage(value = StoragePathMacros.WORKSPACE_FILE, roamingType = RoamingType.DEFAULT)} | ||
) | ||
public final class TimeTrackerComponent implements ProjectComponent, PersistentStateComponent<TimeTrackerState> { | ||
|
||
private final Project project; | ||
private TimeTrackerWidget widget; | ||
|
||
private TimeTrackerState lastStateCache = null; | ||
|
||
public TimeTrackerComponent(Project project) { | ||
this.project = project; | ||
} | ||
|
||
@Override | ||
public void initComponent() { | ||
} | ||
|
||
@Override | ||
public void disposeComponent() { | ||
} | ||
|
||
@Override | ||
@NotNull | ||
public String getComponentName() { | ||
return "TimeTrackerComponent"; | ||
} | ||
|
||
@Override | ||
public void projectOpened() { | ||
if (widget == null) { | ||
widget = new TimeTrackerWidget(); | ||
if (lastStateCache != null) { | ||
widget.setState(lastStateCache); | ||
lastStateCache = null; | ||
} | ||
WindowManager.getInstance().getStatusBar(project).addWidget(widget); | ||
} | ||
} | ||
|
||
@Override | ||
public void projectClosed() { | ||
if (widget != null) { | ||
WindowManager.getInstance().getStatusBar(project).removeWidget(widget.ID()); | ||
lastStateCache = widget.getState(); | ||
} | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public TimeTrackerState getState() { | ||
if (widget != null) { | ||
return widget.getState(); | ||
} else if(lastStateCache != null) { | ||
return lastStateCache; | ||
} else { | ||
return new TimeTrackerState(); | ||
} | ||
} | ||
|
||
@Override | ||
public void loadState(TimeTrackerState state) { | ||
if (widget != null) { | ||
widget.setState(state); | ||
} else { | ||
lastStateCache = state; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.darkyen; | ||
|
||
/** | ||
* | ||
*/ | ||
@SuppressWarnings("WeakerAccess") | ||
public final class TimeTrackerState { | ||
public long totalTimeSeconds = 0; | ||
public long idleThresholdMs = 2 * 60 * 1000; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
package com.darkyen; | ||
|
||
import com.intellij.ide.ui.UISettings; | ||
import com.intellij.openapi.wm.CustomStatusBarWidget; | ||
import com.intellij.openapi.wm.StatusBar; | ||
import com.intellij.openapi.wm.StatusBarWidget; | ||
import com.intellij.ui.JBColor; | ||
import com.intellij.util.concurrency.EdtExecutorService; | ||
import com.intellij.util.ui.JBUI; | ||
import com.intellij.util.ui.UIUtil; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
import javax.swing.*; | ||
import java.awt.*; | ||
import java.awt.event.AWTEventListener; | ||
import java.time.Duration; | ||
import java.util.concurrent.ScheduledFuture; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
/** | ||
* | ||
*/ | ||
public final class TimeTrackerWidget extends JButton implements CustomStatusBarWidget, AWTEventListener { | ||
|
||
private TimeTrackerState state = new TimeTrackerState(); | ||
|
||
private boolean running = false; | ||
private long startedAtMs = 0; | ||
|
||
private boolean idle = false; | ||
private long lastActivityAtMs = System.currentTimeMillis(); | ||
|
||
private ScheduledFuture<?> ticker; | ||
|
||
TimeTrackerWidget() { | ||
addActionListener(e -> setRunning(!running)); | ||
setBorder(StatusBarWidget.WidgetBorder.INSTANCE); | ||
setOpaque(false); | ||
setFocusable(false); | ||
} | ||
|
||
void setState(TimeTrackerState state) { | ||
this.state = state; | ||
} | ||
|
||
synchronized TimeTrackerState getState() { | ||
final long runningForSeconds = runningForSeconds(); | ||
if (runningForSeconds > 0) { | ||
state.totalTimeSeconds += runningForSeconds; | ||
startedAtMs += runningForSeconds * 1000; | ||
} | ||
return state; | ||
} | ||
|
||
private long runningForSeconds() { | ||
if (!running) { | ||
return 0; | ||
} else { | ||
return Math.max(System.currentTimeMillis() - startedAtMs, 0) / 1000; | ||
} | ||
} | ||
|
||
private synchronized void setRunning(boolean running) { | ||
if (!this.running && running) { | ||
this.running = true; | ||
this.startedAtMs = System.currentTimeMillis(); | ||
|
||
if (ticker != null) { | ||
ticker.cancel(false); | ||
} | ||
ticker = EdtExecutorService.getScheduledExecutorInstance().scheduleWithFixedDelay(() -> UIUtil.invokeLaterIfNeeded(() -> { | ||
final long now = System.currentTimeMillis(); | ||
if (now - lastActivityAtMs > state.idleThresholdMs) { | ||
if (this.running) { | ||
setRunning(false); | ||
idle = true; | ||
} | ||
} | ||
repaint(); | ||
}), 1, 1, TimeUnit.SECONDS); | ||
} else if(this.running && !running) { | ||
state.totalTimeSeconds += runningForSeconds(); | ||
this.running = false; | ||
|
||
if (ticker != null) { | ||
ticker.cancel(false); | ||
ticker = null; | ||
} | ||
} | ||
} | ||
|
||
@NotNull | ||
@Override | ||
public String ID() { | ||
return "com.darkyen.DarkyenusTimeTracker"; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public WidgetPresentation getPresentation(@NotNull PlatformType type) { | ||
return null; | ||
} | ||
|
||
@Override | ||
public void install(@NotNull StatusBar statusBar) { | ||
Toolkit.getDefaultToolkit().addAWTEventListener(this, | ||
AWTEvent.KEY_EVENT_MASK | | ||
AWTEvent.MOUSE_EVENT_MASK | | ||
AWTEvent.MOUSE_MOTION_EVENT_MASK | ||
); | ||
} | ||
|
||
@Override | ||
public void dispose() { | ||
Toolkit.getDefaultToolkit().removeAWTEventListener(this); | ||
setRunning(false); | ||
} | ||
|
||
private static final Color COLOR_OFF = new JBColor(new Color(189, 0, 16), new Color(128, 0, 0)); | ||
private static final Color COLOR_ON = new JBColor(new Color(28, 152, 19), new Color(56, 113, 41)); | ||
private static final Color COLOR_IDLE = new JBColor(new Color(200, 164, 23), new Color(163, 112, 17)); | ||
|
||
@Override | ||
public void paintComponent(final Graphics g) { | ||
long result; | ||
synchronized (this) { | ||
result = state.totalTimeSeconds + runningForSeconds(); | ||
} | ||
final String info = formatDuration(result); | ||
|
||
final Dimension size = getSize(); | ||
final Insets insets = getInsets(); | ||
|
||
final int totalBarLength = size.width - insets.left - insets.right; | ||
final int barHeight = Math.max(size.height, getFont().getSize() + 2); | ||
final int yOffset = (size.height - barHeight) / 2; | ||
final int xOffset = insets.left; | ||
|
||
g.setColor(running ? COLOR_ON : (idle ? COLOR_IDLE : COLOR_OFF)); | ||
g.fillRect(insets.left, insets.bottom, totalBarLength, size.height - insets.bottom - insets.top); | ||
|
||
final Color fg = getModel().isPressed() ? UIUtil.getLabelDisabledForeground() : JBColor.foreground(); | ||
g.setColor(fg); | ||
UISettings.setupAntialiasing(g); | ||
g.setFont(getWidgetFont()); | ||
final FontMetrics fontMetrics = g.getFontMetrics(); | ||
final int infoWidth = fontMetrics.charsWidth(info.toCharArray(), 0, info.length()); | ||
final int infoHeight = fontMetrics.getAscent(); | ||
g.drawString(info, xOffset + (totalBarLength - infoWidth) / 2, yOffset + infoHeight + (barHeight - infoHeight) / 2 - 1); | ||
} | ||
|
||
private static String formatDuration(long secondDuration) { | ||
final Duration duration = Duration.ofSeconds(secondDuration); | ||
final StringBuilder sb = new StringBuilder(); | ||
|
||
boolean found = false; | ||
final long days = duration.toDays(); | ||
if(days != 0) { | ||
found = true; | ||
sb.append(days).append(" day"); | ||
if (days != 1) { | ||
sb.append("s"); | ||
} | ||
} | ||
final long hours = duration.toHours() % 24; | ||
if(found || hours != 0) { | ||
if(found) { | ||
sb.append(" "); | ||
} | ||
found = true; | ||
sb.append(hours).append(" hour"); | ||
if (hours != 1) { | ||
sb.append("s"); | ||
} | ||
} | ||
final long minutes = duration.toMinutes() % 60; | ||
if(found || minutes != 0) { | ||
if(found) { | ||
sb.append(" "); | ||
} | ||
found = true; | ||
sb.append(minutes).append(" min");/* | ||
if (minutes != 1) { | ||
sb.append("s"); | ||
}*/ | ||
} | ||
final long seconds = duration.getSeconds() % 60; | ||
{ | ||
if(found) { | ||
sb.append(" "); | ||
} | ||
sb.append(seconds).append(" sec");/* | ||
if (seconds != 1) { | ||
sb.append("s"); | ||
}*/ | ||
} | ||
return sb.toString(); | ||
} | ||
|
||
@Override | ||
public JComponent getComponent() { | ||
return this; | ||
} | ||
|
||
private static Font getWidgetFont() { | ||
return JBUI.Fonts.label(11); | ||
} | ||
|
||
private static final String SAMPLE_STRING = formatDuration(999999999999L); | ||
@Override | ||
public Dimension getPreferredSize() { | ||
final Insets insets = getInsets(); | ||
int width = getFontMetrics(getWidgetFont()).stringWidth(SAMPLE_STRING) + insets.left + insets.right + JBUI.scale(2); | ||
int height = getFontMetrics(getWidgetFont()).getHeight() + insets.top + insets.bottom + JBUI.scale(2); | ||
return new Dimension(width, height); | ||
} | ||
|
||
@Override | ||
public Dimension getMinimumSize() { | ||
return getPreferredSize(); | ||
} | ||
|
||
@Override | ||
public Dimension getMaximumSize() { | ||
return getPreferredSize(); | ||
} | ||
|
||
@Override | ||
public void eventDispatched(AWTEvent event) { | ||
lastActivityAtMs = System.currentTimeMillis(); | ||
if (idle) { | ||
idle = false; | ||
setRunning(true); | ||
} | ||
} | ||
} |