package com.darkyen; import com.intellij.icons.AllIcons; import com.intellij.ide.ui.UISettings; import com.intellij.openapi.ui.popup.ComponentPopupBuilder; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.ui.popup.JBPopupListener; import com.intellij.openapi.ui.popup.LightweightWindowEvent; 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.ui.awt.RelativePoint; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import static com.darkyen.TimeTrackingStatus.RUNNING; /** * The custom widget that is the main UI of the plugin. * * NOTES: * - Not implementing getPresentation(), because it is not used for CustomStatusBarWidgets. * - AWTEventListener is for inactivity listening */ public final class TimeTrackerWidget extends JButton implements CustomStatusBarWidget { // Synchronized with xml public static final String ID = "com.darkyen.DarkyenusTimeTracker"; @NotNull private final TimeTrackerService service; private boolean mouseInside = false; private enum PopupState { HIDDEN, VISIBLE, VISIBLE_WIDGET_PATTERN_FOCUS, VISIBLE_GIT_PATTERN_FOCUS, // Action only VISIBLE_LOST_FOCUS } private PopupState popupState = PopupState.HIDDEN; TimeTrackerWidget(@NotNull TimeTrackerService service) { this.service = service; addActionListener(e -> { final AWTEvent event = EventQueue.getCurrentEvent(); if (event instanceof MouseEvent) { final MouseEvent mouseEvent = (MouseEvent) event; final int width = getWidth(); final Insets insets = getInsets(); final int totalBarLength = width - insets.left - insets.right; final int resumeStopWidth = resumeStopButtonWidth(totalBarLength); final int actionSplit = insets.left + resumeStopWidth; if (mouseEvent.getX() <= actionSplit) { service.toggleRunning(); } else { popupSettings(); } } }); setBorder(StatusBarWidget.WidgetBorder.INSTANCE); setOpaque(false); setFocusable(false); addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { mouseInside = true; repaint(); } @Override public void mouseExited(MouseEvent e) { mouseInside = false; repaint(); } }); } private void setPopupState(@NotNull PopupState newState) { final PopupState oldState = this.popupState; if (newState == oldState) { return; } switch (newState) { case HIDDEN: case VISIBLE: this.popupState = newState; break; case VISIBLE_WIDGET_PATTERN_FOCUS: case VISIBLE_GIT_PATTERN_FOCUS: if (oldState != PopupState.HIDDEN) { this.popupState = newState; } break; case VISIBLE_LOST_FOCUS: if (oldState != PopupState.HIDDEN) { this.popupState = PopupState.VISIBLE; } break; } repaint(); revalidate(); } private static int resumeStopButtonWidth(int widgetWidth) { return widgetWidth - Math.max(widgetWidth / 5, SETTINGS_ICON.getIconWidth() / 2 * 3); } private void popupSettings() { final TimeTrackerPopupContent content = new TimeTrackerPopupContent(service, (patternFieldType) -> { if (patternFieldType == null) { setPopupState(PopupState.VISIBLE_LOST_FOCUS); } else if (patternFieldType == TimeTrackerPopupContent.PatternField.WIDGET) { setPopupState(PopupState.VISIBLE_WIDGET_PATTERN_FOCUS); } else if (patternFieldType == TimeTrackerPopupContent.PatternField.GIT) { setPopupState(PopupState.VISIBLE_GIT_PATTERN_FOCUS); } }); final ComponentPopupBuilder popupBuilder = JBPopupFactory.getInstance().createComponentPopupBuilder(content, null); popupBuilder.setCancelOnClickOutside(true); popupBuilder.setFocusable(true); popupBuilder.setRequestFocus(true); popupBuilder.setShowBorder(true); popupBuilder.setShowShadow(true); final JBPopup popup = popupBuilder.createPopup(); content.popup = popup; final Rectangle visibleRect = TimeTrackerWidget.this.getVisibleRect(); final Dimension preferredSize = content.getPreferredSize(); final RelativePoint point = new RelativePoint(TimeTrackerWidget.this, new Point(visibleRect.x+visibleRect.width - preferredSize.width, visibleRect.y - (preferredSize.height + 15))); popup.show(point); popup.addListener(new JBPopupListener() { @Override public void onClosed(@NotNull LightweightWindowEvent event) { setPopupState(PopupState.HIDDEN); } }); // Not sure if needed, but sometimes the popup is not clickable for some mysterious reason // and it stopped happening when this was added content.requestFocus(); setPopupState(PopupState.VISIBLE); } @NotNull @Override public String ID() { return ID; } @Override public void install(@NotNull StatusBar statusBar) {} @Override public void dispose() {} private TimePattern currentShowTimePattern() { final PopupState popupState = this.popupState; switch (popupState) { case HIDDEN: case VISIBLE_WIDGET_PATTERN_FOCUS: default: return service.getIdeTimePattern(); case VISIBLE: case VISIBLE_LOST_FOCUS: return FULL_TIME_FORMATTING; case VISIBLE_GIT_PATTERN_FOCUS: return service.getGitTimePattern(); } } @Override public void paintComponent(final Graphics g) { final int timeToShow = service.getTotalTimeSeconds(); final String info = currentShowTimePattern().secondsToString(timeToShow); 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; final TimeTrackingStatus status = service.getStatus(); if (mouseInside) { if (status == RUNNING) { g.setColor(COLOR_MENU_ON); } else { g.setColor(COLOR_MENU_OFF); } } else { switch (status) { case RUNNING: g.setColor(COLOR_ON); break; case IDLE: g.setColor(COLOR_IDLE); break; case STOPPED: g.setColor(COLOR_OFF); break; } } g.fillRect(insets.left, insets.bottom, totalBarLength, size.height - insets.bottom - insets.top); UISettings.setupAntialiasing(g); if (mouseInside) { // Draw controls g.setColor(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()); final int resumeStopWidth = resumeStopButtonWidth(totalBarLength); final int settingsWidth = totalBarLength - resumeStopWidth; g.drawLine(xOffset + resumeStopWidth, yOffset, xOffset + resumeStopWidth, yOffset + barHeight); Icon firstIcon = status == RUNNING ? STOP_ICON : START_ICON; firstIcon.paintIcon(this, g, xOffset + (resumeStopWidth - firstIcon.getIconWidth()) / 2, yOffset + (barHeight - firstIcon.getIconHeight())/2); SETTINGS_ICON.paintIcon(this, g, xOffset + resumeStopWidth + (settingsWidth - SETTINGS_ICON.getIconWidth()) / 2, yOffset + (barHeight - SETTINGS_ICON.getIconHeight())/2); } else { // Draw time text final Color fg = getModel().isPressed() ? UIUtil.getLabelDisabledForeground() : JBColor.foreground(); g.setColor(fg); g.setFont(WIDGET_FONT); 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); } } @Override public JComponent getComponent() { return this; } private TimePattern getPreferredSize_lastPattern = null; private Font getPreferredSize_lastFont = null; private int getPreferredSize_lastWidth = -1; @Override public Dimension getPreferredSize() { final Font widgetFont = WIDGET_FONT; final FontMetrics fontMetrics = getFontMetrics(widgetFont); final TimePattern pattern = currentShowTimePattern(); final int stringWidth; if (widgetFont.equals(getPreferredSize_lastFont) && pattern.equals(getPreferredSize_lastPattern)) { stringWidth = getPreferredSize_lastWidth; } else { int maxWidth = 0; // Size may decrease with growing time, so we try different second boundaries for (int seconds : PREFERRED_SIZE_SECOND_QUERIES) { maxWidth = Math.max(maxWidth, fontMetrics.stringWidth(pattern.secondsToString(seconds - 1))); } getPreferredSize_lastPattern = pattern; getPreferredSize_lastFont = widgetFont; getPreferredSize_lastWidth = maxWidth; stringWidth = maxWidth; } final Insets insets = getInsets(); int width = stringWidth + insets.left + insets.right + JBUI.scale(2); int height = fontMetrics.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(); } private static final Icon SETTINGS_ICON = AllIcons.General.Settings; private static final Icon START_ICON = AllIcons.Actions.Resume; private static final Icon STOP_ICON = AllIcons.Actions.Pause; private static final Font WIDGET_FONT = JBUI.Fonts.label(11); 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)); private static final Color COLOR_MENU_OFF = new JBColor(new Color(198, 88, 97), new Color(97, 38, 38)); private static final Color COLOR_MENU_ON = new JBColor(new Color(133, 194, 130), new Color(55, 80, 48)); public static final TimePattern FULL_TIME_FORMATTING = TimePattern.parse("{{lw \"week\"s}} {{ld \"day\"s}} {{lh \"hour\"s}} {{lm \"minute\"s}} {{s \"second\"s}}"); private static final int[] PREFERRED_SIZE_SECOND_QUERIES = { 60, 60 * 60, 60 * 60 * 24, 60 * 60 * 24 * 7, 1999999999 }; }