All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.progress.spi.Controller Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.netbeans.modules.progress.spi;

import java.util.*;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.progress.module.DefaultHandleFactory;
import org.netbeans.progress.module.TrivialProgressBaseWorkerProvider;
import org.openide.util.RequestProcessor;

/**
 *
 * @author Milos Kleint ([email protected])
 * @since org.netbeans.api.progress/1 1.18
 */
public class Controller {
    private static final Logger LOG = Logger.getLogger(Controller.class.getName());
    
    // non-private so that it can be accessed from the tests
    public static Controller defaultInstance;
    
    private ProgressUIWorker component;
    private TaskModel model;
    private List eventQueue;
    private boolean dispatchRunning;
    private long timerStart = 0;
    private static final int TIMER_QUANTUM = 400;
    
    /**
     * initial delay for ading progress indication into the UI. if finishes earlier,
     * not shown at all, applies just to the status line (default) comtroller.
     */
    public static final int INITIAL_DELAY = 500;
    
    /** Creates a new instance of Controller */
    @SuppressWarnings("OverridableMethodCallInConstructor")
    public Controller(ProgressUIWorker comp) {
        component = comp;
        model = new TaskModel(getEventExecutor());
        eventQueue = new LinkedList();
        dispatchRunning = false;
    }

    public static synchronized Controller getDefault() {
        if (defaultInstance == null) {
            ProgressEnvironment f = DefaultHandleFactory.get();
            defaultInstance = f.getController();
        }
        return defaultInstance;
    }
    
    /**
     * Creates a worker. The default implementation creates a trivial no-op
     * worker. Subclasses should override to create a meaningful worker implementation.
     * @return worker instance 
     */
    protected ProgressUIWorkerWithModel createWorker() {
        Logger.getLogger(Controller.class.getName()).log(Level.CONFIG, "Using fallback trivial progress implementation");
        return new TrivialProgressBaseWorkerProvider().getDefaultWorker();
    }
    
    /**
     * Retrieves the ProgressUIWorker instance.
     * The instance is lazily created by calling {@link #createWorker}.
     * @return ProgressUIWorker instance
     */
    protected final ProgressUIWorker getProgressUIWorker()
    {
        if (component == null)
        {
            ProgressUIWorkerWithModel prgUIWorker = createWorker();
            prgUIWorker.setModel(getDefault().getModel());
            component = prgUIWorker;
        }
        return component;
    }

    public TaskModel getModel() {
        return model;
    }
    
    void start(InternalHandle handle) {
        ProgressEvent event = new ProgressEvent(handle, ProgressEvent.TYPE_START, isWatched(handle));
        if (this == getDefault() && handle.getInitialDelay() > 100) {
            // default controller
            postEvent(event, true);
        } else {
            runImmediately(Collections.singleton(event));
        }
    }
    
    void finish(InternalHandle handle) {
        ProgressEvent event = new ProgressEvent(handle, ProgressEvent.TYPE_FINISH, isWatched(handle));
        postEvent(event);
    }
    
    void toIndeterminate(InternalHandle handle) {
        ProgressEvent event = new ProgressEvent(handle, ProgressEvent.TYPE_SWITCH, isWatched(handle));
        model.updateSelection();
        postEvent(event);
    }
    
    void toSilent(InternalHandle handle, String message) {
        ProgressEvent event = new ProgressEvent(handle, ProgressEvent.TYPE_SILENT, isWatched(handle), message);
        model.updateSelection();
        postEvent(event);
    }
    
    
    void toDeterminate(InternalHandle handle) {
        ProgressEvent event = new ProgressEvent(handle, ProgressEvent.TYPE_SWITCH, isWatched(handle));
        model.updateSelection();
        postEvent(event);
    }    
    
    void progress(InternalHandle handle, String msg, 
                  int units, double percentage, long estimate) {
        ProgressEvent event = new ProgressEvent(handle, msg, units, percentage, estimate, isWatched(handle));
        postEvent(event);
    }
    
    ProgressEvent snapshot(InternalHandle handle, String msg, 
                  int units, double percentage, long estimate) {
        if (handle.isInSleepMode()) {
            return new ProgressEvent(handle, ProgressEvent.TYPE_SILENT, isWatched(handle), msg);
        }
        return new ProgressEvent(handle, msg, units, percentage, estimate, isWatched(handle));
    }
    
    
    void explicitSelection(InternalHandle handle) {
        InternalHandle old = model.getExplicitSelection();
        model.explicitlySelect(handle);
        Collection evnts = new ArrayList();
        evnts.add(handle.requestStateSnapshot());
        if (old != null && old != handle) {
            // refresh the old one, results in un-bodling the text.
            evnts.add(old.requestStateSnapshot());
        }
        runImmediately(evnts);
    }
    
    void displayNameChange(InternalHandle handle, int units, double percentage, long estimate, String display) {
        Collection evnts = new ArrayList();
        evnts.add(new ProgressEvent(handle, null, units, percentage, estimate, isWatched(handle), display));
        runImmediately(evnts);
    }
    
    private boolean isWatched(InternalHandle hndl) {
        return model.getExplicitSelection() == hndl;
    }
    
    void runImmediately(Collection events) {
        synchronized (this) {
            // need to add to queue immediately in the current thread
            eventQueue.addAll(events);
            dispatchRunning = true;
        }
        // trigger ui update as fast as possible.
        runEvents();
    }
    
    void postEvent(final ProgressEvent event) {
        postEvent(event, false);
    }
    
    /**
     * Schedules a callback after `delay' milliseconds. If there's some initial
     * delay, and `shorten' is true, the delay is shortened to be minimum of the current
     * delay and the `delay'.
     * 

* The scheduler ticks can be disabled if the specified `delay' is 0. *

* This method is to be implemented by environment-specific subclass, to use the target * platform scheduling support to invoke the 'runNow' method after the specified period. *

* The default implementation uses a private RequestProcessor thread. * @param delay the delay after which the callback should be fired. -1 means no chage to the configured delay. 0 means to stop * @param shorten if the current delay is shorter than the `delay', keep the current one. If false, always configure the delay * @param activate restart ticks. If false, just configure the scheduler. */ final void schedule(int delay, boolean shorten, boolean activate) { if (delay != -1) { if (shorten && taskDelay < delay) { delay = taskDelay; } } else { delay = taskDelay; } this.taskDelay = delay; resetTimer(delay, activate); } private int taskDelay = TIMER_QUANTUM; private static final RequestProcessor RQ = new RequestProcessor(Controller.class.getName()); private final RequestProcessor.Task task = RQ.create(new Runnable() { public void run() { runEvents(); } }); void postEvent(final ProgressEvent event, boolean shortenPeriod) { synchronized (this) { eventQueue.add(event); if (!dispatchRunning) { timerStart = System.currentTimeMillis(); dispatchRunning = true; schedule(shortenPeriod ? event.getSource().getInitialDelay() : -1, shortenPeriod, true); } else if (shortenPeriod) { // time remaining is longer than required by the handle's initial delay. // restart with shorter time. if (System.currentTimeMillis() - timerStart > event.getSource().getInitialDelay()) { schedule(event.getSource().getInitialDelay(), false, true); } } } } /** * The method is responsible to start or stop a timing service in the environment. * If "delay" is 0 or less, the method should stop the timer, or at least not call * the {@link #runEvents} if the timer ticks. *

* If "delay" is positive, the method should configure the timer to fire after * "delay" milliseconds. Depending on "restart" parameter, the timer should * be just configured (false), or activated (true) - potentially canceling the previous * schedule. *

* The default implementation uses {@link RequestProcessor} for scheduling. * * @param delay delay in milliseconds before the timer should fire * @param activate if true, activate the changes immediately. */ protected void resetTimer(int delay, boolean activate) { if (delay > 0) { if (activate) { task.schedule(delay); } } else { assert activate; task.cancel(); } } /** * Executes runnable synchronously with controller's event dispatch. * The method is used by Progress API to deliver other events which do not * therefore interfere with Controller's own events. If called from the thread * dispatching events, the executed Runnables will be delayed after the * and may be interleaved by dispatched events. *

* The method must be overriden by a controller implementation which * changes the threading model from the default one. * * @return the executor instance * @since 1.44 */ protected Executor getEventExecutor() { return RQ; } /** * Processes the queued events. Depending on environment, planning to a specific thread, * or passing some specific information may be necessary. */ protected void runEvents() { RQ.execute(new Runnable() { public void run() { runNow(); } }); } public void runNow() { // not true in tests: assert EventQueue.isDispatchThread(); HashMap map = new HashMap(); boolean hasShortOne = false; long minDiff = TIMER_QUANTUM; InternalHandle oldSelected = model.getSelectedHandle(); long stamp = System.currentTimeMillis(); synchronized (this) { Iterator it = eventQueue.iterator(); Collection justStarted = new ArrayList(); while (it.hasNext()) { ProgressEvent event = it.next(); boolean isShort = (stamp - event.getSource().getTimeStampStarted()) < event.getSource().getInitialDelay(); if (event.getType() == ProgressEvent.TYPE_START) { if (event.getSource().isCustomPlaced() || !isShort) { LOG.log(Level.FINER, "Adding to model {0}", event); model.addHandle(event.getSource()); } else { LOG.log(Level.FINER, "Short-start: {0}", event); justStarted.add(event.getSource()); } } else if (event.getType() == ProgressEvent.TYPE_FINISH && (! justStarted.contains(event.getSource()))) { LOG.log(Level.FINER, "Removed from model: {0}", event); model.removeHandle(event.getSource()); } ProgressEvent lastEvent = map.get(event.getSource()); if (lastEvent != null && event.getType() == ProgressEvent.TYPE_FINISH && justStarted.contains(event.getSource()) && isShort) { // if task quits really fast, ignore.. // defined 'really fast' as being shorter than initial delay LOG.log(Level.FINER, "Short task ended: {0}", event); map.remove(event.getSource()); justStarted.remove(event.getSource()); } else { if (lastEvent != null) { // preserve last message if (LOG.isLoggable(Level.FINER)) { LOG.log(Level.FINE, "Merging event " + event.toString()); } event.copyMessageFromEarlier(lastEvent); // preserve the switched state if (lastEvent.isSwitched()) { event.markAsSwitched(); } // preserve finish type if (lastEvent.getType() == ProgressEvent.TYPE_FINISH) { event.markAsFinished(); } LOG.log(Level.FINER, "Event merged with {0} to {1}", new Object [] { lastEvent, event} ); } map.put(event.getSource(), event); } it.remove(); } // now re-add the just started events into queue // if they don't last longer than the initial delay of the task. // applies just for status bar items Iterator startIt = justStarted.iterator(); while (startIt.hasNext()) { InternalHandle hndl = startIt.next(); long diff = stamp - hndl.getTimeStampStarted(); if (diff >= hndl.getInitialDelay()) { model.addHandle(hndl); } else { ProgressEvent stE; eventQueue.add(stE = new ProgressEvent(hndl, ProgressEvent.TYPE_START, isWatched(hndl))); LOG.log(Level.FINER, "Repost start event: {0}", stE); ProgressEvent evnt = map.remove(hndl); if (evnt.getType() != ProgressEvent.TYPE_START) { LOG.log(Level.FINER, "Repost queued event: {0}", evnt); eventQueue.add(evnt); } hasShortOne = true; minDiff = Math.min(minDiff, hndl.getInitialDelay() - diff); } } } InternalHandle selected = model.getSelectedHandle(); selected = selected == null ? oldSelected : selected; Iterator it = map.values().iterator(); if (component == null) { getProgressUIWorker(); } while (it.hasNext()) { ProgressEvent event = it.next(); LOG.log(Level.FINER, "Dispatching: {0}", event); if (selected == event.getSource()) { component.processSelectedProgressEvent(event); } component.processProgressEvent(event); } synchronized (this) { schedule(0, false, true); if (hasShortOne) { timerStart = System.currentTimeMillis(); schedule((int)Math.max(100, minDiff), false, true); } else { dispatchRunning = false; schedule(TIMER_QUANTUM, false, !eventQueue.isEmpty()); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy