/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kenai.redminenb.timetracker; import com.kenai.redminenb.Redmine; import com.kenai.redminenb.RedmineConnector; import com.kenai.redminenb.issue.RedmineIssue; import com.kenai.redminenb.repository.RedmineRepository; import com.kenai.redminenb.util.TimeUtil; import com.taskadapter.redmineapi.bean.TimeEntry; import com.taskadapter.redmineapi.bean.TimeEntryActivity; import com.taskadapter.redmineapi.bean.TimeEntryFactory; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Date; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingWorker; import javax.swing.Timer; import org.netbeans.api.settings.ConvertAsProperties; import org.netbeans.modules.bugtracking.api.RepositoryManager; import org.openide.DialogDescriptor; import org.openide.DialogDisplayer; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.windows.TopComponent; import org.openide.util.NbBundle.Messages; import org.openide.windows.WindowManager; /** * Top component which displays something. */ @ConvertAsProperties( dtd = "-//com.kenai.redminenb//IssueTimeTracker//EN", autostore = false ) @TopComponent.Description( preferredID = IssueTimeTrackerTopComponent.PREFERRED_ID, iconBase = "com/kenai/redminenb/resources/redmine.png", persistenceType = TopComponent.PERSISTENCE_ALWAYS ) @TopComponent.Registration(mode = "properties", openAtStartup = false) @ActionID(category = "Window", id = "com.kenai.redminenb.timetracker.IssueTimeTrackerTopComponent") @ActionReference(path = "Menu/Window/Tools", position = 1041, separatorBefore = 1040) @TopComponent.OpenActionRegistration( displayName = "#CTL_IssueTimeTrackerAction", preferredID = "IssueTimeTrackerTopComponent" ) @Messages({ "CTL_IssueTimeTrackerAction=Redmine Issue TimeTracker", "CTL_IssueTimeTrackerTopComponent=Redmine Issue TimeTracker", "MSG_Time={0} hours", "MSG_Time_Running={0} hours (running)", "LBL_Hours=hours", "MSG_NoIssue=No issue selected", "MSG_Issue={0} - {1}", "MSG_Change_Running_Issue=Time tracking for ''{0}'' is running!", "TTL_Change_Running_Issue=Set issue for time tracking", "BTN_Change_Running_Issue_Cancel=Cancel", "BTN_Change_Running_Issue_Save=Save time", "BTN_Change_Running_Issue_Reset=Reset timer", "LBL_TimeTrackingActivity=Activity", "LBL_TimeTrackingComment=Comment", "LBL_Repository=Repository", "LBL_Issue=Issue", "LBL_Time=Time", "LBL_LogTime=Log time", "LBL_SaveFailed=Saving time entry failed", "BTN_StartTracking=Start", "BTN_StopTracking=Stop",}) public final class IssueTimeTrackerTopComponent extends TopComponent { private static final Logger LOG = Logger.getLogger(IssueTimeTrackerTopComponent.class.getName()); @SuppressFBWarnings(value = "CI_CONFUSED_INHERITANCE", justification = "Needed to be usable from annotation") protected static final String PREFERRED_ID = "IssueTimeTrackerTopComponent"; public static final String PROP_ISSUE = "issue"; public static final String PROP_RUNNING = "running"; private final Timer refreshTimer = new Timer(1000, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { refreshDisplay(); } }); private boolean running; private Date start; private long savedTime; private RedmineIssue issue; public static IssueTimeTrackerTopComponent getInstance() { return (IssueTimeTrackerTopComponent) WindowManager.getDefault().findTopComponent(PREFERRED_ID); } public IssueTimeTrackerTopComponent() { initComponents(); setName(Bundle.CTL_IssueTimeTrackerTopComponent()); putClientProperty(TopComponent.PROP_MAXIMIZATION_DISABLED, Boolean.TRUE); putClientProperty(TopComponent.PROP_DND_COPY_DISABLED, Boolean.TRUE); refreshDisplay(); } private long currentTime() { return savedTime + (start == null ? 0 : (new Date().getTime() - start.getTime())); } private void openIssue() { Redmine.getInstance().getSupport().openIssue( issue.getRepository(), issue); } private void refreshDisplay() { long timeInMS = currentTime(); if (issue == null) { repositoryOutputLabel.setText(Bundle.MSG_NoIssue()); issueOutputLabel.setText(Bundle.MSG_NoIssue()); issueOutputLabel.setEnabled(false); timeOutputLabel.setText(Bundle.MSG_Time(TimeUtil.millisecondsToDecimalHours(0l))); saveButton.setEnabled(false); resetButton.setEnabled(false); startButton.setEnabled(false); } else { repositoryOutputLabel.setText(issue.getRepository().getDisplayName()); issueOutputLabel.setText(Bundle.MSG_Issue(issue.getID(), issue.getSummary())); issueOutputLabel.setEnabled(true); startButton.setEnabled(true); if (running) { resetButton.setEnabled(false); saveButton.setEnabled(false); startButton.setText(Bundle.BTN_StopTracking()); timeOutputLabel.setText(Bundle.MSG_Time_Running(TimeUtil.millisecondsToDecimalHours(timeInMS))); } else { startButton.setText(Bundle.BTN_StartTracking()); if (savedTime > 0) { saveButton.setEnabled(true); resetButton.setEnabled(true); } else { saveButton.setEnabled(false); resetButton.setEnabled(false); } timeOutputLabel.setText(Bundle.MSG_Time(TimeUtil.millisecondsToDecimalHours(timeInMS))); } } } public boolean reset() { boolean result; if (!running) { savedTime = 0; result = true; } else { result = false; } refreshDisplay(); return result; } public void save() { if (running) { return; } TimeEntryForm tef = new TimeEntryForm(); tef.setIssue(issue); tef.setTime(savedTime); DialogDescriptor dd = new DialogDescriptor(tef, Bundle.LBL_LogTime()); Object result = DialogDisplayer.getDefault().notify(dd); if (result == DialogDescriptor.OK_OPTION) { final TimeEntry te = TimeEntryFactory.create(); TimeEntryActivity tea = tef.getTimeEntryActivity(); te.setActivityId(tea.getId()); te.setComment(tef.getComment()); te.setHours(((float) tef.getTime()) / (60 * 60 * 1000)); te.setIssueId(tef.getIssue().getIssue().getId()); new SwingWorker() { @Override protected Object doInBackground() throws Exception { issue.getRepository().getTimeEntryManager().createTimeEntry(te); issue.refresh(); return null; } @Override protected void done() { try { get(); savedTime = 0; } catch (InterruptedException ex) { Redmine.LOG.log(Level.SEVERE, Bundle.LBL_SaveFailed(), ex); } catch (ExecutionException ex) { Redmine.LOG.log(Level.SEVERE, Bundle.LBL_SaveFailed(), ex.getCause()); } } }.execute(); } } public void setRunning(boolean running) { if (issue == null) { running = false; } boolean old = this.running; if (running != old) { this.running = running; if (running) { start = new Date(); } else { savedTime = currentTime(); start = null; } firePropertyChange(PROP_RUNNING, old, running); refreshDisplay(); } } public boolean isRunning() { return running; } public RedmineIssue getIssue() { return issue; } private boolean checkIssueChange(RedmineIssue oldIssue, RedmineIssue newIssue) { if (running || savedTime > 0) { String cancel = Bundle.BTN_Change_Running_Issue_Cancel(); String save = Bundle.BTN_Change_Running_Issue_Save(); String reset = Bundle.BTN_Change_Running_Issue_Reset(); DialogDescriptor dd = new DialogDescriptor( Bundle.MSG_Change_Running_Issue(oldIssue.getDisplayName()), Bundle.TTL_Change_Running_Issue()); dd.setOptions(new Object[]{cancel, reset, save}); dd.setClosingOptions(new Object[]{cancel, reset, save}); Object result = DialogDisplayer.getDefault().notify(dd); if (result.equals(reset) || result.equals(save)) { setRunning(false); if (result.equals(reset)) { reset(); } else { save(); } return true; } else { return false; } } return true; } public void setIssue(RedmineIssue issue) { RedmineIssue old = this.issue; if (Objects.equals(old, issue)) { return; } if (checkIssueChange(old, issue)) { this.issue = issue; firePropertyChange(PROP_ISSUE, old, issue); refreshDisplay(); } } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { java.awt.GridBagConstraints gridBagConstraints; jPanel1 = new javax.swing.JPanel(); filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 32767)); issueLabel = new javax.swing.JLabel(); jPanel2 = new javax.swing.JPanel(); resetButton = new javax.swing.JButton(); startButton = new javax.swing.JButton(); saveButton = new javax.swing.JButton(); timeLabel = new javax.swing.JLabel(); timeOutputLabel = new javax.swing.JLabel(); repositoryLabel = new javax.swing.JLabel(); repositoryOutputLabel = new javax.swing.JLabel(); issueOutputLabel = new com.kenai.redminenb.util.LinkButton(); setLayout(new java.awt.GridBagLayout()); jPanel1.setOpaque(false); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE_LEADING; add(jPanel1, gridBagConstraints); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 10; gridBagConstraints.gridy = 10; gridBagConstraints.weightx = 1.0; gridBagConstraints.weighty = 1.0; add(filler1, gridBagConstraints); issueLabel.setFont(issueLabel.getFont().deriveFont(issueLabel.getFont().getStyle() & ~java.awt.Font.BOLD)); org.openide.awt.Mnemonics.setLocalizedText(issueLabel, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.issueLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 1; gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE_LEADING; add(issueLabel, gridBagConstraints); jPanel2.setOpaque(false); org.openide.awt.Mnemonics.setLocalizedText(resetButton, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.resetButton.text")); // NOI18N resetButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { resetButtonActionPerformed(evt); } }); jPanel2.add(resetButton); org.openide.awt.Mnemonics.setLocalizedText(startButton, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.startButton.text")); // NOI18N startButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { startButtonActionPerformed(evt); } }); jPanel2.add(startButton); org.openide.awt.Mnemonics.setLocalizedText(saveButton, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.saveButton.text")); // NOI18N saveButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { saveButtonActionPerformed(evt); } }); jPanel2.add(saveButton); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 3; gridBagConstraints.gridwidth = 2; gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE_LEADING; add(jPanel2, gridBagConstraints); timeLabel.setFont(timeLabel.getFont().deriveFont(timeLabel.getFont().getStyle() & ~java.awt.Font.BOLD)); org.openide.awt.Mnemonics.setLocalizedText(timeLabel, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.timeLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 2; gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE_LEADING; add(timeLabel, gridBagConstraints); timeOutputLabel.setFont(timeOutputLabel.getFont().deriveFont(timeOutputLabel.getFont().getStyle() | java.awt.Font.BOLD)); org.openide.awt.Mnemonics.setLocalizedText(timeOutputLabel, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.timeOutputLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 2; gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE_LEADING; add(timeOutputLabel, gridBagConstraints); repositoryLabel.setFont(repositoryLabel.getFont().deriveFont(repositoryLabel.getFont().getStyle() & ~java.awt.Font.BOLD)); org.openide.awt.Mnemonics.setLocalizedText(repositoryLabel, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.repositoryLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE_LEADING; add(repositoryLabel, gridBagConstraints); repositoryOutputLabel.setFont(repositoryOutputLabel.getFont().deriveFont(repositoryOutputLabel.getFont().getStyle() | java.awt.Font.BOLD)); org.openide.awt.Mnemonics.setLocalizedText(repositoryOutputLabel, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.repositoryOutputLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE_LEADING; add(repositoryOutputLabel, gridBagConstraints); issueOutputLabel.setBorder(null); org.openide.awt.Mnemonics.setLocalizedText(issueOutputLabel, org.openide.util.NbBundle.getMessage(IssueTimeTrackerTopComponent.class, "IssueTimeTrackerTopComponent.issueOutputLabel.text")); // NOI18N issueOutputLabel.setEnabled(false); issueOutputLabel.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { issueOutputLabelActionPerformed(evt); } }); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 1; gridBagConstraints.anchor = java.awt.GridBagConstraints.BASELINE_LEADING; add(issueOutputLabel, gridBagConstraints); }// </editor-fold>//GEN-END:initComponents private void startButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_startButtonActionPerformed this.setRunning(!this.isRunning()); }//GEN-LAST:event_startButtonActionPerformed private void resetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_resetButtonActionPerformed this.reset(); }//GEN-LAST:event_resetButtonActionPerformed private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed save(); }//GEN-LAST:event_saveButtonActionPerformed private void issueOutputLabelActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_issueOutputLabelActionPerformed openIssue(); }//GEN-LAST:event_issueOutputLabelActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.Box.Filler filler1; private javax.swing.JLabel issueLabel; private com.kenai.redminenb.util.LinkButton issueOutputLabel; private javax.swing.JPanel jPanel1; private javax.swing.JPanel jPanel2; private javax.swing.JLabel repositoryLabel; private javax.swing.JLabel repositoryOutputLabel; private javax.swing.JButton resetButton; private javax.swing.JButton saveButton; private javax.swing.JButton startButton; private javax.swing.JLabel timeLabel; private javax.swing.JLabel timeOutputLabel; // End of variables declaration//GEN-END:variables @Override protected void componentHidden() { refreshTimer.stop(); } @Override protected void componentShowing() { refreshTimer.start(); } void writeProperties(java.util.Properties p) { p.setProperty("version", "1.0"); if (issue != null) { long currentTime = currentTime(); p.setProperty("connector", RedmineConnector.ID); p.setProperty("repository", issue.getRepository().getID()); p.setProperty("issue", issue.getID()); if (currentTime > 0) { p.setProperty("time", Long.toString(currentTime)); } } } void readProperties(java.util.Properties p) { String version = p.getProperty("version"); if (!"1.0".equals(version)) { LOG.warning("Unknown version for TopComponent properties"); } String connector = p.getProperty("connector"); String repository = p.getProperty("repository"); String issueId = p.getProperty("issue"); if (connector != null && repository != null && issueId != null) { // Make sure repository is initialized via the issue API, no ... // this is a crude hack: // @todo: Find a better way to access "out" implementation RepositoryManager.getInstance().getRepository(connector, repository); RedmineRepository rr = RedmineRepository.getInstanceyById(repository); if (rr != null) { RedmineIssue ri = rr.getIssue(issueId); if (ri != null) { this.setIssue(ri); } String time = p.getProperty("time", "0"); try { this.savedTime = Long.parseLong(time); } catch (NumberFormatException ex) { } } } refreshDisplay(); } }