ccbb4f8afcc4fbac29441fc86db62dc283f185db
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / progress / util / ProgressWindow.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.progress.util;
17
18 import com.intellij.ide.IdeEventQueue;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.ui.DialogWrapper;
24 import com.intellij.openapi.ui.DialogWrapperPeer;
25 import com.intellij.openapi.ui.impl.FocusTrackbackProvider;
26 import com.intellij.openapi.ui.impl.GlassPaneDialogWrapperPeer;
27 import com.intellij.openapi.util.Comparing;
28 import com.intellij.openapi.util.Condition;
29 import com.intellij.openapi.util.Disposer;
30 import com.intellij.openapi.util.EmptyRunnable;
31 import com.intellij.openapi.wm.IdeFocusManager;
32 import com.intellij.openapi.wm.WindowManager;
33 import com.intellij.openapi.wm.ex.WindowManagerEx;
34 import com.intellij.ui.FocusTrackback;
35 import com.intellij.ui.PopupBorder;
36 import com.intellij.ui.TitlePanel;
37 import com.intellij.ui.awt.RelativePoint;
38 import com.intellij.ui.components.JBLabel;
39 import com.intellij.util.Alarm;
40 import com.intellij.util.ui.UIUtil;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43
44 import javax.swing.*;
45 import javax.swing.border.Border;
46 import java.awt.*;
47 import java.awt.event.*;
48 import java.io.File;
49
50 @SuppressWarnings({"NonStaticInitializer"})
51 public class ProgressWindow extends BlockingProgressIndicator implements Disposable {
52   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.progress.util.ProgressWindow");
53
54   /**
55    * This constant defines default delay for showing progress dialog (in millis).
56    *
57    * @see #setDelayInMillis(int)
58    */
59   public static final int DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS = 300;
60
61   private static final int UPDATE_INTERVAL = 50; //msec. 20 frames per second.
62
63   private MyDialog myDialog;
64   private final Alarm myUpdateAlarm = new Alarm(this);
65
66   private final Project myProject;
67   private final boolean myShouldShowCancel;
68   private       String  myCancelText;
69
70   private String myTitle = null;
71
72   private boolean myStoppedAlready = false;
73   protected final FocusTrackback myFocusTrackback;
74   private boolean myStarted      = false;
75   protected boolean myBackgrounded = false;
76   private boolean myWasShown;
77   private String myProcessId = "<unknown>";
78   @Nullable private volatile Runnable myBackgroundHandler;
79   private int myDelayInMillis = DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS;
80
81   public ProgressWindow(boolean shouldShowCancel, Project project) {
82     this(shouldShowCancel, false, project);
83   }
84
85   public ProgressWindow(boolean shouldShowCancel, boolean shouldShowBackground, @Nullable Project project) {
86     this(shouldShowCancel, shouldShowBackground, project, null);
87   }
88
89   public ProgressWindow(boolean shouldShowCancel, boolean shouldShowBackground, @Nullable Project project, String cancelText) {
90     this(shouldShowCancel, shouldShowBackground, project, null, cancelText);
91   }
92
93   public ProgressWindow(boolean shouldShowCancel,
94                         boolean shouldShowBackground,
95                         @Nullable Project project,
96                         JComponent parentComponent,
97                         String cancelText) {
98     myProject = project;
99     myShouldShowCancel = shouldShowCancel;
100     myCancelText = cancelText;
101     setModalityProgress(shouldShowBackground ? null : this);
102     myFocusTrackback = new FocusTrackback(this, WindowManager.getInstance().suggestParentWindow(project), false);
103
104     Component parent = parentComponent;
105     if (parent == null && project == null) {
106       parent = JOptionPane.getRootFrame();
107     }
108
109     if (parent != null) {
110       myDialog = new MyDialog(shouldShowBackground, parent, myCancelText);
111     }
112     else {
113       myDialog = new MyDialog(shouldShowBackground, myProject, myCancelText);
114     }
115
116     Disposer.register(this, myDialog);
117
118     myFocusTrackback.registerFocusComponent(myDialog.getPanel());
119   }
120
121   public synchronized void start() {
122     LOG.assertTrue(!isRunning());
123     LOG.assertTrue(!myStoppedAlready);
124
125     super.start();
126     if (!ApplicationManager.getApplication().isUnitTestMode()) {
127       prepareShowDialog();
128     }
129
130     myStarted = true;
131   }
132
133   /**
134    * There is a possible case that many short (in terms of time) progress tasks are executed in a small amount of time.
135    * Problem: UI blinks and looks ugly if we show progress dialog for every such task (every dialog disappears shortly).
136    * Solution is to postpone showing progress dialog in assumption that the task may be already finished when it's
137    * time to show the dialog.
138    * <p/>
139    * Default value is {@link #DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS}
140    *
141    * @param delayInMillis   new delay time in milliseconds
142    */
143   public void setDelayInMillis(int delayInMillis) {
144     myDelayInMillis = delayInMillis;
145   }
146
147   private synchronized boolean isStarted() {
148     return myStarted;
149   }
150
151   protected void prepareShowDialog() {
152     UIUtil.invokeLaterIfNeeded(new Runnable() {
153       @Override
154       public void run() {
155         // We know at least about one use-case that requires special treatment here: many short (in terms of time) progress tasks are
156         // executed in a small amount of time. Problem: UI blinks and looks ugly if we show progress dialog that disappears shortly
157         // for each of them. Solution is to postpone the tasks of showing progress dialog. Hence, it will not be shown at all
158         // if the task is already finished when the time comes.
159         Timer timer = UIUtil.createNamedTimer("Progress window timer",myDelayInMillis, new ActionListener() {
160           @Override
161           public void actionPerformed(ActionEvent e) {
162             if (isRunning()) {
163               if (myDialog != null) {
164                 final DialogWrapper popup = myDialog.myPopup;
165                 if (popup != null) {
166                   myFocusTrackback.registerFocusComponent(new FocusTrackback.ComponentQuery() {
167                     @SuppressWarnings({"ConstantConditions"})
168                     public Component getComponent() {
169                       return popup.getPreferredFocusedComponent();
170                     }
171                   });
172                   if (popup.isShowing()) {
173                     myWasShown = true;
174                   }
175                 }
176               }
177               showDialog();
178             }
179             else {
180               Disposer.dispose(ProgressWindow.this);
181             }
182           }
183         });
184         timer.setRepeats(false);
185         timer.start();
186       }
187     });
188   }
189
190   public void startBlocking() {
191     ApplicationManager.getApplication().assertIsDispatchThread();
192     synchronized (this) {
193       LOG.assertTrue(!isRunning());
194       LOG.assertTrue(!myStoppedAlready);
195     }
196
197     enterModality();
198
199     IdeEventQueue.getInstance().pumpEventsForHierarchy(myDialog.myPanel, new Condition<AWTEvent>() {
200       public boolean value(final AWTEvent object) {
201         if (myShouldShowCancel &&
202             object instanceof KeyEvent &&
203             object.getID() == KeyEvent.KEY_PRESSED &&
204             ((KeyEvent)object).getKeyCode() == KeyEvent.VK_ESCAPE &&
205             ((KeyEvent)object).getModifiers() == 0) {
206           SwingUtilities.invokeLater(new Runnable() {
207             public void run() {
208               cancel();
209             }
210           });
211         }
212         return isStarted() && !isRunning();
213       }
214     });
215
216     exitModality();
217   }
218
219   @NotNull
220   public String getProcessId() {
221     return myProcessId;
222   }
223
224   public void setProcessId(@NotNull String processId) {
225     myProcessId = processId;
226   }
227
228   protected void showDialog() {
229     if (!isRunning() || isCanceled()) {
230       return;
231     }
232
233     myWasShown = true;
234     myDialog.show();
235     if (myDialog != null) {
236       myDialog.myRepaintRunnable.run();
237     }
238   }
239
240   public void setIndeterminate(boolean indeterminate) {
241     super.setIndeterminate(indeterminate);
242     update();
243   }
244
245   public synchronized void stop() {
246     LOG.assertTrue(!myStoppedAlready);
247
248     super.stop();
249
250     if (isDialogShowing()) {
251       if (myFocusTrackback != null) {
252         myFocusTrackback.setWillBeSheduledForRestore();
253       }      
254     }
255
256     UIUtil.invokeLaterIfNeeded(new Runnable() {
257       @Override
258       public void run() {
259         boolean wasShowing = isDialogShowing();
260         if (myDialog != null) {
261           myDialog.hide();
262         }
263         
264         if (myFocusTrackback != null) {
265           if (wasShowing) {
266             myFocusTrackback.restoreFocus();
267           } else {
268             myFocusTrackback.consume();
269           }
270         }
271
272         myStoppedAlready = true;
273
274         Disposer.dispose(ProgressWindow.this);
275       }
276     });
277
278     SwingUtilities.invokeLater(EmptyRunnable.INSTANCE); // Just to give blocking dispatching a chance to go out.
279   }
280
281   private boolean isDialogShowing() {
282     return myDialog != null && myDialog.getPanel() != null && myDialog.getPanel().isShowing();
283   }
284
285   public void cancel() {
286     super.cancel();
287     if (myDialog != null) {
288       myDialog.cancel();
289     }
290   }
291
292   public void background() {
293     final Runnable backgroundHandler = myBackgroundHandler;
294     if (backgroundHandler != null) {
295       backgroundHandler.run();
296       return;
297     }
298
299     if (myDialog != null) {
300       myBackgrounded = true;
301       myDialog.background();
302
303       if (myDialog.wasShown()) {
304         myFocusTrackback.restoreFocus();
305       }
306       else {
307         myFocusTrackback.consume();
308       }
309
310       myDialog = null;
311     }
312   }
313
314   public boolean isBackgrounded() {
315     return myBackgrounded;
316   }
317
318   public void setText(String text) {
319     if (!Comparing.equal(text, getText())) {
320       super.setText(text);
321       update();
322     }
323   }
324
325   public void setFraction(double fraction) {
326     if (fraction != getFraction()) {
327       super.setFraction(fraction);
328       update();
329     }
330   }
331
332   public void setText2(String text) {
333     if (!Comparing.equal(text, getText2())) {
334       super.setText2(text);
335       update();
336     }
337   }
338
339   private void update() {
340     if (myDialog != null) {
341       myDialog.update();
342     }
343   }
344
345   public void setTitle(String title) {
346     if (!Comparing.equal(title, myTitle)) {
347       myTitle = title;
348       update();
349     }
350   }
351
352   public String getTitle() {
353     return myTitle;
354   }
355
356   protected static int getPercentage(double fraction) {
357     return (int)(fraction * 99 + 0.5);
358   }
359
360   protected class MyDialog implements Disposable {
361     private long myLastTimeDrawn = -1;
362     private volatile boolean myShouldShowBackground;
363
364     private final Runnable myRepaintRunnable = new Runnable() {
365       public void run() {
366         String text = getText();
367         double fraction = getFraction();
368         String text2 = getText2();
369
370         myTextLabel.setText(text != null && !text.isEmpty() ? text : " ");
371
372         if (myProgressBar.isShowing()) {
373           final int perc = (int)(fraction * 100);
374           myProgressBar.setIndeterminate(perc == 0 || isIndeterminate());
375           myProgressBar.setValue(perc);
376         }
377
378         myText2Label.setText(getTitle2Text(text2, myText2Label.getWidth()));
379
380         myTitlePanel.setText(myTitle != null && !myTitle.isEmpty() ? myTitle : " ");
381
382         myLastTimeDrawn = System.currentTimeMillis();
383         myRepaintedFlag = true;
384       }
385     };
386
387     private String getTitle2Text(String fullText, int labelWidth) {
388       if (fullText == null || fullText.isEmpty()) return " ";
389       while (myText2Label.getFontMetrics(myText2Label.getFont()).stringWidth(fullText) > labelWidth) {
390         int sep = fullText.indexOf(File.separatorChar, 4);
391         if (sep < 0) return fullText;
392         fullText = "..." + fullText.substring(sep);
393       }
394
395       return fullText;
396     }
397
398     private final Runnable myUpdateRequest = new Runnable() {
399       public void run() {
400         update();
401       }
402     };
403
404     private JPanel myPanel;
405
406     private JLabel myTextLabel;
407     private JBLabel myText2Label;
408
409     private JButton myCancelButton;
410     private JButton myBackgroundButton;
411
412     private JProgressBar myProgressBar;
413     private boolean myRepaintedFlag = true;
414     private       TitlePanel    myTitlePanel;
415     private       DialogWrapper myPopup;
416     private final Window        myParentWindow;
417     private       Point         myLastClicked;
418
419     public MyDialog(boolean shouldShowBackground, Project project, String cancelText) {
420       Window parentWindow = WindowManager.getInstance().suggestParentWindow(project);
421       if (parentWindow == null) {
422         parentWindow = WindowManagerEx.getInstanceEx().getMostRecentFocusedWindow();
423       }
424       myParentWindow = parentWindow;
425
426       initDialog(shouldShowBackground, cancelText);
427     }
428
429     public MyDialog(boolean shouldShowBackground, Component parent, String cancelText) {
430       myParentWindow = parent instanceof Window
431                        ? (Window)parent
432                        : (Window)SwingUtilities.getAncestorOfClass(Window.class, parent);
433       initDialog(shouldShowBackground, cancelText);
434     }
435
436     private void initDialog(boolean shouldShowBackground, String cancelText) {
437       if (UIUtil.isUnderAquaLookAndFeel()) {
438         UIUtil.applyStyle(UIUtil.ComponentStyle.SMALL, myText2Label);
439       }
440       myProgressBar.setPreferredSize(new Dimension(UIUtil.isUnderAquaLookAndFeel() ? 350 : 450, -1));
441
442       myCancelButton.addActionListener(new ActionListener() {
443         public void actionPerformed(ActionEvent e) {
444           doCancelAction();
445         }
446       });
447
448       myCancelButton.registerKeyboardAction(new ActionListener() {
449         public void actionPerformed(ActionEvent e) {
450           if (myCancelButton.isEnabled()) {
451             doCancelAction();
452           }
453         }
454       }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
455
456       myShouldShowBackground = shouldShowBackground;
457       if (cancelText != null) {
458         setCancelButtonText(cancelText);
459       }
460       myProgressBar.setMaximum(100);
461       createCenterPanel();
462
463       myTitlePanel.setActive(true);
464       myTitlePanel.addMouseListener(new MouseAdapter() {
465         public void mousePressed(MouseEvent e) {
466           final Point titleOffset = RelativePoint.getNorthWestOf(myTitlePanel).getScreenPoint();
467           myLastClicked = new RelativePoint(e).getScreenPoint();
468           myLastClicked.x -= titleOffset.x;
469           myLastClicked.y -= titleOffset.y;
470         }
471       });
472
473       myTitlePanel.addMouseMotionListener(new MouseMotionAdapter() {
474         public void mouseDragged(MouseEvent e) {
475           if (myLastClicked == null) {
476             return;
477           }
478           final Point draggedTo = new RelativePoint(e).getScreenPoint();
479           draggedTo.x -= myLastClicked.x;
480           draggedTo.y -= myLastClicked.y;
481
482           if (myPopup != null) {
483             myPopup.setLocation(draggedTo);
484           }
485         }
486       });
487     }
488
489     public void dispose() {
490       UIUtil.disposeProgress(myProgressBar);
491       UIUtil.dispose(myTitlePanel);
492     }
493
494     public JPanel getPanel() {
495       return myPanel;
496     }
497
498     public void setShouldShowBackground(final boolean shouldShowBackground) {
499       myShouldShowBackground = shouldShowBackground;
500       SwingUtilities.invokeLater(new Runnable() {
501         public void run() {
502           myBackgroundButton.setVisible(shouldShowBackground);
503           myPanel.revalidate();
504         }
505       });
506     }
507
508     public void changeCancelButtonText(String text) {
509       myCancelButton.setText(text);
510     }
511
512     public void doCancelAction() {
513       if (myShouldShowCancel) {
514         ProgressWindow.this.cancel();
515       }
516     }
517
518     public void cancel() {
519       if (myShouldShowCancel) {
520         SwingUtilities.invokeLater(new Runnable() {
521           public void run() {
522             myCancelButton.setEnabled(false);
523           }
524         });
525       }
526     }
527
528     private void createCenterPanel() {
529       // Cancel button (if any)
530
531       if (myCancelText != null) {
532         myCancelButton.setText(myCancelText);
533       }
534       myCancelButton.setVisible(myShouldShowCancel);
535
536       myBackgroundButton.setVisible(myShouldShowBackground);
537       myBackgroundButton.addActionListener(
538         new ActionListener() {
539           public void actionPerformed(ActionEvent e) {
540             if (myShouldShowBackground) {
541               ProgressWindow.this.background();
542             }
543           }
544         }
545       );
546     }
547
548     private synchronized void update() {
549       if (myRepaintedFlag) {
550         if (System.currentTimeMillis() > myLastTimeDrawn + UPDATE_INTERVAL) {
551           myRepaintedFlag = false;
552           SwingUtilities.invokeLater(myRepaintRunnable);
553         }
554         else {
555           if (myUpdateAlarm.getActiveRequestCount() == 0) {
556             myUpdateAlarm.addRequest(myUpdateRequest, 500, getModalityState());
557           }
558         }
559       }
560     }
561
562     public synchronized void background() {
563       if (myShouldShowBackground) {
564         myBackgroundButton.setEnabled(false);
565       }
566
567       hide();
568     }
569
570     public void hide() {
571       SwingUtilities.invokeLater(new Runnable() {
572         public void run() {
573           if (myPopup != null) {
574             myPopup.close(DialogWrapper.CANCEL_EXIT_CODE);
575             myPopup = null;
576           }
577         }
578       });
579     }
580
581     public void show() {
582       if (ApplicationManager.getApplication().isHeadlessEnvironment()) return;
583       if (myParentWindow == null) return;
584       if (myPopup != null) {
585         myPopup.close(DialogWrapper.CANCEL_EXIT_CODE);
586       }
587
588       myPopup = myParentWindow.isShowing()
589                 ? new MyDialogWrapper(myParentWindow, myShouldShowCancel)
590                 : new MyDialogWrapper(myProject, myShouldShowCancel);
591       myPopup.setUndecorated(true);
592       myPopup.pack();
593
594       SwingUtilities.invokeLater(new Runnable() {
595         public void run() {
596           if (myPopup != null) {
597             if (myPopup.getPeer() instanceof FocusTrackbackProvider) {
598               final FocusTrackback focusTrackback = ((FocusTrackbackProvider)myPopup.getPeer()).getFocusTrackback();
599               if (focusTrackback != null) {
600                 focusTrackback.consume();
601               }
602             }
603
604             getFocusManager().requestFocus(myCancelButton, true);
605           }
606         }
607       });
608
609       myPopup.show();
610     }
611
612     public boolean wasShown() {
613       return myWasShown;
614     }
615
616     private class MyDialogWrapper extends DialogWrapper {
617       private final boolean myIsCancellable;
618
619       public MyDialogWrapper(Project project, final boolean cancellable) {
620         super(project, false);
621         init();
622         myIsCancellable = cancellable;
623       }
624
625       public MyDialogWrapper(Component parent, final boolean cancellable) {
626         super(parent, false);
627         init();
628         myIsCancellable = cancellable;
629       }
630
631       @Override
632       public void doCancelAction() {
633         if (myIsCancellable) {
634           super.doCancelAction();
635         }
636       }
637
638       @Override
639       protected DialogWrapperPeer createPeer(final Component parent, final boolean canBeParent) {
640         if (System.getProperty("vintage.progress") == null) {
641           try {
642             return new GlassPaneDialogWrapperPeer(this, parent, canBeParent);
643           }
644           catch (GlassPaneDialogWrapperPeer.GlasspanePeerUnavailableException e) {
645             return super.createPeer(parent, canBeParent);
646           }
647         }
648         else {
649           return super.createPeer(parent, canBeParent);
650         }
651       }
652
653       @Override
654       protected DialogWrapperPeer createPeer(final boolean canBeParent, final boolean toolkitModalIfPossible) {
655         if (System.getProperty("vintage.progress") == null) {
656           try {
657             return new GlassPaneDialogWrapperPeer(this, canBeParent);
658           }
659           catch (GlassPaneDialogWrapperPeer.GlasspanePeerUnavailableException e) {
660             return super.createPeer(canBeParent, toolkitModalIfPossible);
661           }
662         }
663         else {
664           return super.createPeer(canBeParent, toolkitModalIfPossible);
665         }
666       }
667
668       @Override
669       protected DialogWrapperPeer createPeer(final Project project, final boolean canBeParent) {
670         if (System.getProperty("vintage.progress") == null) {
671           try {
672             return new GlassPaneDialogWrapperPeer(this, project, canBeParent);
673           }
674           catch (GlassPaneDialogWrapperPeer.GlasspanePeerUnavailableException e) {
675             return super.createPeer(project, canBeParent);
676           }
677         }
678         else {
679           return super.createPeer(project, canBeParent);
680         }
681       }
682
683       protected void init() {
684         super.init();
685         setUndecorated(true);
686         myPanel.setBorder(PopupBorder.Factory.create(true, true));
687       }
688
689       protected boolean isProgressDialog() {
690         return true;
691       }
692
693       protected JComponent createCenterPanel() {
694         return myPanel;
695       }
696
697       @Nullable
698       protected JComponent createSouthPanel() {
699         return null;
700       }
701
702       @Nullable
703       protected Border createContentPaneBorder() {
704         return null;
705       }
706     }
707   }
708
709   public void setBackgroundHandler(@Nullable Runnable backgroundHandler) {
710     myBackgroundHandler = backgroundHandler;
711     myDialog.setShouldShowBackground(backgroundHandler != null);
712   }
713
714   public void setCancelButtonText(String text) {
715     if (myDialog != null) {
716       myDialog.changeCancelButtonText(text);
717     }
718     else {
719       myCancelText = text;
720     }
721   }
722
723   private IdeFocusManager getFocusManager() {
724     return IdeFocusManager.getInstance(myProject);
725   }
726
727   public void dispose() {
728   }
729
730   public boolean isPopupWasShown() {
731     return myDialog != null && myDialog.myPopup != null && myDialog.myPopup.isShowing();
732   }
733 }