001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.progress.spi;
043:
044: import java.awt.event.ActionEvent;
045: import java.util.logging.Level;
046: import java.util.logging.Logger;
047: import javax.swing.Action;
048: import javax.swing.JComponent;
049: import javax.swing.JLabel;
050: import org.netbeans.progress.module.*;
051: import org.openide.util.Cancellable;
052: import org.openide.util.Lookup;
053:
054: /**
055: * Instances provided by the ProgressHandleFactory allow the users of the API to
056: * notify the progress bar UI about changes in the state of the running task.
057: * @author Milos Kleint (mkleint@netbeans.org)
058: */
059: public final class InternalHandle {
060:
061: private static final Logger LOG = Logger
062: .getLogger(InternalHandle.class.getName());
063:
064: private String displayName;
065: private boolean customPlaced1 = false;
066: private boolean customPlaced2 = false;
067: private boolean customPlaced3 = false;
068: private int state;
069: private int totalUnits;
070: private int currentUnit;
071: private long initialEstimate;
072: private long timeStarted;
073: private long timeLastProgress;
074: private long timeSleepy = 0;
075: private String lastMessage;
076: private final Cancellable cancelable;
077: private final Action viewAction;
078: private final boolean userInitiated;
079: private int initialDelay = Controller.INITIAL_DELAY;
080: private Controller controller;
081: private ExtractedProgressUIWorker component;
082:
083: public static final int STATE_INITIALIZED = 0;
084: public static final int STATE_RUNNING = 1;
085: public static final int STATE_FINISHED = 2;
086: public static final int STATE_REQUEST_STOP = 3;
087:
088: public static final int NO_INCREASE = -2;
089:
090: /** Creates a new instance of ProgressHandle */
091: public InternalHandle(String displayName, Cancellable cancel,
092: boolean userInitiated, Action view) {
093: this .displayName = displayName;
094: this .userInitiated = userInitiated;
095: state = STATE_INITIALIZED;
096: totalUnits = 0;
097: lastMessage = null;
098: cancelable = cancel;
099: viewAction = view;
100: }
101:
102: public String getDisplayName() {
103: return displayName;
104: }
105:
106: /**
107: * XXX - called from UI, threading
108: */
109: public synchronized int getState() {
110: return state;
111: }
112:
113: public boolean isAllowCancel() {
114: return cancelable != null && !isCustomPlaced();
115: }
116:
117: public boolean isAllowView() {
118: return viewAction != null && !isCustomPlaced();
119: }
120:
121: public boolean isCustomPlaced() {
122: return component != null;
123: }
124:
125: public boolean isUserInitialized() {
126: return userInitiated;
127: }
128:
129: private int getCurrentUnit() {
130: return currentUnit;
131: }
132:
133: public int getTotalUnits() {
134: return totalUnits;
135: }
136:
137: public void setInitialDelay(int millis) {
138: if (state != STATE_INITIALIZED) {
139: LOG
140: .warning("Setting ProgressHandle.setInitialDelay() after the task is started has no effect"); //NOI18N
141: return;
142: }
143: initialDelay = millis;
144: }
145:
146: public int getInitialDelay() {
147: return initialDelay;
148: }
149:
150: public synchronized void toSilent(String message) {
151: if (state != STATE_RUNNING && state != STATE_REQUEST_STOP) {
152: assert false : "cannot switch to silent mode when not running";
153: }
154: timeLastProgress = System.currentTimeMillis();
155: timeSleepy = timeLastProgress;
156: if (message != null) {
157: lastMessage = message;
158: }
159: controller.toSilent(this , message);
160: }
161:
162: public boolean isInSleepMode() {
163: return timeSleepy == timeLastProgress;
164: }
165:
166: public synchronized void toIndeterminate() {
167: if (state != STATE_RUNNING && state != STATE_REQUEST_STOP) {
168: assert false : "cannot switch to indeterminate mode when not running: "
169: + state;
170: }
171: totalUnits = 0;
172: currentUnit = 0;
173: initialEstimate = -1;
174: timeLastProgress = System.currentTimeMillis();
175: controller.toIndeterminate(this );
176: }
177:
178: public synchronized void toDeterminate(int workunits, long estimate) {
179: if (state != STATE_RUNNING && state != STATE_REQUEST_STOP) {
180: assert false : "cannot switch to determinate mode when not running";
181: }
182: if (workunits < 0) {
183: throw new IllegalArgumentException(
184: "number of workunits cannot be negative");
185: }
186: totalUnits = workunits;
187: currentUnit = 0;
188: initialEstimate = estimate;
189: timeLastProgress = System.currentTimeMillis();
190: controller.toDeterminate(this );
191: }
192:
193: /**
194: * start the progress indication for a task with known number of steps and known
195: * time estimate for completing the task.
196: *
197: * @param message
198: * @param workunits
199: * @param estimate estimated time to process the task in seconds
200: */
201: public synchronized void start(String message, int workunits,
202: long estimate) {
203: if (state != STATE_INITIALIZED) {
204: throw new IllegalStateException(
205: "Cannot call start twice on a handle");
206: }
207: if (workunits < 0) {
208: throw new IllegalArgumentException(
209: "number of workunits cannot be negative");
210: }
211: totalUnits = workunits;
212: currentUnit = 0;
213: if (message != null) {
214: lastMessage = message;
215: }
216: if (controller == null) {
217: controller = Controller.getDefault();
218: }
219: state = STATE_RUNNING;
220: initialEstimate = estimate;
221: timeStarted = System.currentTimeMillis();
222: timeLastProgress = timeStarted;
223:
224: controller.start(this );
225: }
226:
227: /**
228: * finish the task, remove the task's component from the progress bar UI.
229: */
230: public synchronized void finish() {
231: if (state == STATE_INITIALIZED) {
232: throw new IllegalStateException(
233: "Cannot finish not a started task");
234: }
235: if (state == STATE_FINISHED) {
236: return;
237: }
238: state = STATE_FINISHED;
239: currentUnit = totalUnits;
240:
241: controller.finish(this );
242: }
243:
244: /**
245: *
246: * @param message
247: * @param workunit
248: */
249: public synchronized void progress(String message, int workunit) {
250: if (state != STATE_RUNNING && state != STATE_REQUEST_STOP) {
251: return;
252: }
253:
254: if (workunit != NO_INCREASE) {
255: if (workunit < currentUnit) {
256: throw new IllegalArgumentException(
257: "Cannot decrease processed workunit count ("
258: + workunit
259: + ") to lower value than before ("
260: + currentUnit + ")");
261: }
262: if (workunit > totalUnits) {
263: // seems to be the by far most frequently abused contract. Record it to log file and safely handle the case
264: //#96921 - WARNING -> INFO to prevent users reporting the problem automatically.
265: LOG
266: .log(
267: Level.INFO,
268: "Cannot process more work than scheduled. "
269: + "Progress handle with name \""
270: + getDisplayName()
271: + "\" has requested progress to workunit no."
272: + workunit
273: + " but the total number of workunits is "
274: + totalUnits
275: + ". That means the progress bar UI will not display real progress and will stay at 100%.",
276: new IllegalArgumentException());
277: workunit = totalUnits;
278: }
279: currentUnit = workunit;
280: }
281: if (message != null) {
282: lastMessage = message;
283: }
284: timeLastProgress = System.currentTimeMillis();
285:
286: controller
287: .progress(this , message, currentUnit,
288: totalUnits > 0 ? getPercentageDone() : -1,
289: (initialEstimate == -1 ? -1
290: : calculateFinishEstimate()));
291: }
292:
293: // XXX - called from UI, threading
294:
295: public void requestCancel() {
296: if (!isAllowCancel()) {
297: return;
298: }
299: synchronized (this ) {
300: state = STATE_REQUEST_STOP;
301: }
302: // do not call in synchronized block because it can take a long time to process,
303: /// and it could slow down UI.
304: //TODO - call in some other thread, not AWT? what is the cancel() contract?
305: cancelable.cancel();
306: synchronized (this ) {
307: requestStateSnapshot();
308: }
309: }
310:
311: ///XXX - called from UI, threading
312: public void requestView() {
313: if (!isAllowView()) {
314: return;
315: }
316: viewAction.actionPerformed(new ActionEvent(viewAction,
317: ActionEvent.ACTION_PERFORMED, "performView"));
318: }
319:
320: // XXX - called from UI, threading
321: public synchronized void requestExplicitSelection() {
322: if (!isInSleepMode()) {
323: timeLastProgress = System.currentTimeMillis();
324: }
325: controller.explicitSelection(this );
326: }
327:
328: public synchronized void requestDisplayNameChange(
329: String newDisplayName) {
330: displayName = newDisplayName;
331: if (state == STATE_INITIALIZED) {
332: return;
333: }
334: timeLastProgress = System.currentTimeMillis();
335: controller
336: .displayNameChange(this , currentUnit,
337: totalUnits > 0 ? getPercentageDone() : -1,
338: (initialEstimate == -1 ? -1
339: : calculateFinishEstimate()),
340: newDisplayName);
341: }
342:
343: // XXX - called from UI, threading
344: public synchronized ProgressEvent requestStateSnapshot() {
345: if (!isInSleepMode()) {
346: timeLastProgress = System.currentTimeMillis();
347: }
348: return controller
349: .snapshot(this , lastMessage, currentUnit,
350: totalUnits > 0 ? getPercentageDone() : -1,
351: (initialEstimate == -1 ? -1
352: : calculateFinishEstimate()));
353: }
354:
355: private void createExtractedWorker() {
356: if (component == null) {
357: ProgressUIWorkerProvider prov = Lookup.getDefault().lookup(
358: ProgressUIWorkerProvider.class);
359: if (prov == null) {
360: LOG
361: .log(Level.CONFIG,
362: "Using fallback trivial progress implementation");
363: prov = new TrivialProgressUIWorkerProvider();
364: }
365: component = prov.getExtractedComponentWorker();
366: controller = new Controller(component);
367: }
368: }
369:
370: /**
371: * have the component in custom location, don't include in the status bar.
372: */
373: public synchronized JComponent extractComponent() {
374: if (customPlaced1) {
375: throw new IllegalStateException(
376: "Cannot retrieve progress component multiple times");
377: }
378: if (state != STATE_INITIALIZED) {
379: throw new IllegalStateException(
380: "You can request custom placement of progress component only before starting the task");
381: }
382: customPlaced1 = true;
383: createExtractedWorker();
384: return component.getProgressComponent();
385: }
386:
387: public synchronized JLabel extractDetailLabel() {
388: if (customPlaced2) {
389: throw new IllegalStateException(
390: "Cannot retrieve progress detail label component multiple times");
391: }
392: if (state != STATE_INITIALIZED) {
393: throw new IllegalStateException(
394: "You can request custom placement of progress component only before starting the task");
395: }
396: customPlaced2 = true;
397: createExtractedWorker();
398: return component.getDetailLabelComponent();
399: }
400:
401: public synchronized JLabel extractMainLabel() {
402: if (customPlaced3) {
403: throw new IllegalStateException(
404: "Cannot retrieve progress main label component multiple times");
405: }
406: if (state != STATE_INITIALIZED) {
407: throw new IllegalStateException(
408: "You can request custom placement of progress component only before starting the task");
409: }
410: customPlaced3 = true;
411: createExtractedWorker();
412: return component.getMainLabelComponent();
413: }
414:
415: long calculateFinishEstimate() {
416:
417: // we are interested in seconds only
418: double durationSoFar = ((double) (System.currentTimeMillis() - timeStarted)) / 1000;
419: if (initialEstimate == -1) {
420: // we don't have an initial estimate, calculate by real-life data only
421: return (long) (durationSoFar * (totalUnits - currentUnit) / totalUnits);
422: } else {
423: // in the begining give the initial estimate more weight than in the end.
424: // should give us more smooth estimates
425: long remainingUnits = (totalUnits - currentUnit);
426: double remainingPortion = (double) remainingUnits
427: / (double) totalUnits;
428: double currentEstimate = durationSoFar
429: / (double) currentUnit * totalUnits;
430: long retValue = (long) (((initialEstimate * remainingUnits * remainingPortion) + (currentEstimate
431: * remainingUnits * (1 - remainingPortion))) / totalUnits);
432: return retValue;
433: }
434: }
435:
436: /**
437: *public because of tests.
438: */
439: public double getPercentageDone() {
440: return ((double) currentUnit * 100 / (double) totalUnits);
441: }
442:
443: public long getTimeStampStarted() {
444: return timeStarted;
445: }
446:
447: }
|