Skip to content

Commit

Permalink
Initial
Browse files Browse the repository at this point in the history
  • Loading branch information
Darkyenus committed Nov 27, 2016
0 parents commit 443a50a
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/out/
/*.jar
46 changes: 46 additions & 0 deletions resources/META-INF/plugin.xml
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>
81 changes: 81 additions & 0 deletions src/com/darkyen/TimeTrackerComponent.java
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;
}
}
}
10 changes: 10 additions & 0 deletions src/com/darkyen/TimeTrackerState.java
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;
}
237 changes: 237 additions & 0 deletions src/com/darkyen/TimeTrackerWidget.java
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);
}
}
}

0 comments on commit 443a50a

Please sign in to comment.