Revision: 3792
Author: [email protected]
Date: Tue Jul 27 14:11:45 2010
Log: Added the first progress bar for persisting to the server. This is really just a background thread that updates the progress bar until the server responds with the persist calls complete. There is no feedback from the server updating the numbers, it is just an approximation based on previous runs of persist calls that have made it to the server.

The next step is to make the server respond with progress in saving information to the server. We also need to make other users persist calls be noticed by the progress bars to notify users when changes will be incoming.
http://code.google.com/p/power-architect/source/detail?r=3792

Added:
 /trunk/src/main/java/ca/sqlpower/architect/ArchitectStatusInformation.java
 /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectStatusBar.java
 /trunk/src/main/resources/icons/progressBar.png
Modified:
 /trunk/regress/ca/sqlpower/architect/StubArchitectSession.java
 /trunk/regress/ca/sqlpower/architect/TestingArchitectSession.java
/trunk/regress/ca/sqlpower/architect/swingui/TestingArchitectSwingSession.java
 /trunk/src/main/java/ca/sqlpower/architect/ArchitectSession.java
 /trunk/src/main/java/ca/sqlpower/architect/ArchitectSessionImpl.java
/trunk/src/main/java/ca/sqlpower/architect/enterprise/NetworkConflictResolver.java
 /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectFrame.java
/trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectSwingSession.java /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectSwingSessionImpl.java

=======================================
--- /dev/null
+++ /trunk/src/main/java/ca/sqlpower/architect/ArchitectStatusInformation.java Tue Jul 27 14:11:45 2010
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2010, SQL Power Group Inc.
+ *
+ * This file is part of SQL Power Architect.
+ *
+ * SQL Power Architect is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * SQL Power Architect is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package ca.sqlpower.architect;
+
+import ca.sqlpower.util.MonitorableImpl;
+
+/**
+ * Used to update a status message/label/image/text/other that is user visible
+ * but does not interrupt their work.
+ */
+public interface ArchitectStatusInformation {
+
+    /**
+ * Creates and returns an object that can be used to monitor the progress of + * one type of action. If you want to monitor a second type of action call
+     * this method again to get a new monitor for the new action. This will
+     * prevent the progress bars from moving backwards. When the monitor
+ * isFinished method returns true or the progress has reached or passed the
+     * job size the monitor will be removed.
+     */
+    public MonitorableImpl createProgressMonitor();
+
+}
=======================================
--- /dev/null
+++ /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectStatusBar.java Tue Jul 27 14:11:45 2010
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2010, SQL Power Group Inc.
+ *
+ * This file is part of SQL Power Architect.
+ *
+ * SQL Power Architect is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * SQL Power Architect is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package ca.sqlpower.architect.swingui;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.ImageIcon;
+import javax.swing.JPanel;
+
+import org.apache.log4j.Logger;
+
+import ca.sqlpower.architect.ArchitectStatusInformation;
+import ca.sqlpower.util.MonitorableImpl;
+
+import com.jgoodies.forms.builder.DefaultFormBuilder;
+import com.jgoodies.forms.layout.FormLayout;
+
+/**
+ * This class contains a status bar that appears at the bottom of the
+ * ArchitectFrame. The status bar is handy for updating the user on changes that + * are helpful for the user to know but is not so important that they need to be
+ * given a pop-up.
+ */
+public class ArchitectStatusBar implements ArchitectStatusInformation {
+
+ private static final Logger logger = Logger.getLogger(ArchitectStatusBar.class);
+
+ private static final ImageIcon PROGRESS_BAR_ICON = new ImageIcon(ArchitectStatusBar.class.getResource("/icons/progressBar.png"));
+
+    /**
+ * This progress bar updates its UI immediately by painting directly to its + * graphics object on changes to its settings to keep the user informed of + * the current progress of the operation even if the foreground thread that
+     * normally does the painting is blocked waiting for server operations.
+     * <p>
+ * Methods in this class are synchronized as the progress bar can be updated
+     * from multiple threads.
+     */
+    private class ArchitectStatusProgressBar extends MonitorableImpl {
+
+        /**
+         * This is the starting x position on the progress bar where this
+         * progress bar specifically can start to paint.
+         */
+        private int x = 0;
+
+        /**
+         * This is the width of the progress bar this specific progress bar
+         * can update on.
+         */
+        private int width = 0;
+
+        @Override
+        public synchronized void setJobSize(Integer jobSize) {
+            super.setJobSize(jobSize);
+            paint();
+        }
+
+        @Override
+        public synchronized void setMessage(String message) {
+            super.setMessage(message);
+            paint();
+        }
+
+        @Override
+        public synchronized void setProgress(int progress) {
+            super.setProgress(progress);
+            paint();
+            if (progress >= getJobSize()) {
+                removeProgressBar(this);
+            }
+        }
+
+        @Override
+        public synchronized void incrementProgress() {
+            super.incrementProgress();
+            paint();
+            if (getProgress() >= getJobSize()) {
+                removeProgressBar(this);
+            }
+        }
+
+        @Override
+        public synchronized void setCancelled(boolean cancelled) {
+            super.setCancelled(cancelled);
+            paint();
+            removeProgressBar(this);
+        }
+
+        @Override
+        public synchronized void setFinished(boolean finished) {
+            super.setFinished(finished);
+            paint();
+            removeProgressBar(this);
+        }
+
+        private synchronized void paint() {
+            Graphics g = progressBarPanel.getGraphics();
+            progressBarPanel.setDoubleBuffered(true);
+
+            if (!isCancelled() && !isFinished() &&
+                    getJobSize() != null && getProgress() < getJobSize()) {
+ BufferedImage buffer = new BufferedImage(getWidth(), progressBarPanel.getHeight(), BufferedImage.TYPE_INT_ARGB);
+                Graphics bufferG = buffer.getGraphics();
+                bufferG = progressBarPanel.getGraphics();
+
+                g.setColor(progressBarPanel.getBackground());
+ g.fillRect(getX(), 0, getWidth(), progressBarPanel.getHeight());
+                bufferG.drawImage(PROGRESS_BAR_ICON.getImage(), getX(), 0,
+                        (int) (getWidth() * getProgress() / getJobSize()),
+                        progressBarPanel.getHeight(), null);
+
+                bufferG.setColor(Color.BLACK);
+                if (getMessage() != null) {
+
+                    Font font = g.getFont();
+                    int fontSize = font.getSize();
+                    FontMetrics fm = g.getFontMetrics();
+ while (fm.getHeight() > progressBarPanel.getHeight() && fontSize > 0) {
+                        font = font.deriveFont((float) (fontSize - 1));
+                        g.setFont(font);
+                        fm = g.getFontMetrics();
+                        fontSize--;
+                    }
+
+ int fontX = (int) ((getWidth() / 2) - (fm.getStringBounds(getMessage(), g).getWidth() / 2)) + getX(); + int fontY = (progressBarPanel.getHeight() / 2) + (fm.getHeight() / 2);
+                    bufferG.drawString(getMessage(), fontX, fontY);
+                }
+ g.drawImage(buffer, getWidth(), progressBarPanel.getHeight(), null);
+            } else {
+                g.setColor(progressBarPanel.getBackground());
+ g.fillRect(getX(), 0, getWidth(), progressBarPanel.getHeight());
+            }
+        }
+
+        public synchronized void setX(int x) {
+            this.x = x;
+            paint();
+        }
+
+        public synchronized int getX() {
+            return x;
+        }
+
+        public synchronized void setWidth(int width) {
+            this.width = width;
+            paint();
+        }
+
+        public synchronized int getWidth() {
+            return width;
+        }
+
+    }
+
+    /**
+     * The actual panel that appears at the bottom of the ArchitectFrame.
+     * Interesting things like status text and progress bars can be entered
+     * here.
+     */
+    private final JPanel statusBar = new JPanel();
+
+    /**
+ * Different {...@link ArchitectStatusBar} classes will be given this progress + * bar and a section of it that they can repaint for a progress bar. This + * allows the progress to be given to the user even if the EDT thread is + * busy. If we tried to add in other panels and work with them the panel + * would not be added until after the current work of the EDT was completed
+     * which may be too late.
+     */
+    private final JPanel progressBarPanel = new JPanel();
+
+    /**
+     * The existing progress bars in this status bar.
+     */
+    private final List<ArchitectStatusProgressBar> progressBars =
+        new ArrayList<ArchitectStatusProgressBar>();
+
+    public ArchitectStatusBar() {
+ DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("fill:pref:grow"), statusBar);
+        builder.append(progressBarPanel);
+    }
+
+    public JPanel getStatusBar() {
+        return statusBar;
+    }
+
+    @Override
+    public MonitorableImpl createProgressMonitor() {
+ ArchitectStatusProgressBar newBar = new ArchitectStatusProgressBar();
+        progressBars.add(newBar);
+        resizeProgressBars();
+        return newBar;
+    }
+
+    /**
+     * Call to clear a progress bar off of the status bar.
+     */
+    private void removeProgressBar(ArchitectStatusProgressBar bar) {
+        progressBars.remove(bar);
+        resizeProgressBars();
+    }
+
+    private void resizeProgressBars() {
+        Graphics g = progressBarPanel.getGraphics();
+        g.setColor(progressBarPanel.getBackground());
+ g.fillRect(0, 0, progressBarPanel.getWidth(), progressBarPanel.getHeight());
+        if (progressBars.isEmpty())  return;
+        int newWidth = progressBarPanel.getWidth() / progressBars.size();
+        int x = 0;
+        for (ArchitectStatusProgressBar bar : progressBars) {
+            bar.setX(x);
+            bar.setWidth(newWidth);
+            x += newWidth;
+        }
+    }
+
+}
=======================================
--- /dev/null   
+++ /trunk/src/main/resources/icons/progressBar.png     Tue Jul 27 14:11:45 2010
Binary file, no diff available.
=======================================
--- /trunk/regress/ca/sqlpower/architect/StubArchitectSession.java Thu Jul 22 13:35:26 2010 +++ /trunk/regress/ca/sqlpower/architect/StubArchitectSession.java Tue Jul 27 14:11:45 2010
@@ -187,4 +187,10 @@

        public void setLiquibaseSettings(LiquibaseSettings settings) {
        }
-}
+
+    @Override
+    public ArchitectStatusInformation getStatusInformation() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+}
=======================================
--- /trunk/regress/ca/sqlpower/architect/TestingArchitectSession.java Thu Jul 22 13:35:26 2010 +++ /trunk/regress/ca/sqlpower/architect/TestingArchitectSession.java Tue Jul 27 14:11:45 2010
@@ -223,4 +223,10 @@
        public void setLiquibaseSettings(LiquibaseSettings settings) {

        }
-}
+
+    @Override
+    public ArchitectStatusInformation getStatusInformation() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+}
=======================================
--- /trunk/regress/ca/sqlpower/architect/swingui/TestingArchitectSwingSession.java Thu Jul 22 13:35:26 2010 +++ /trunk/regress/ca/sqlpower/architect/swingui/TestingArchitectSwingSession.java Tue Jul 27 14:11:45 2010
@@ -518,4 +518,9 @@
         // TODO Auto-generated method stub

     }
-}
+
+    @Override
+    public ArchitectStatusBar getStatusInformation() {
+        return null;
+    }
+}
=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/ArchitectSession.java Thu Jul 22 13:35:26 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/ArchitectSession.java Tue Jul 27 14:11:45 2010
@@ -185,4 +185,11 @@
         */
        public void setLiquibaseSettings(LiquibaseSettings settings);

-}
+    /**
+ * Returns the status information object to update the user on different + * kinds of progress. May return null if no status information exists (for
+     * headless mode).
+     */
+       public ArchitectStatusInformation getStatusInformation();
+
+}
=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/ArchitectSessionImpl.java Tue Jul 27 09:04:00 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/ArchitectSessionImpl.java Tue Jul 27 14:11:45 2010
@@ -84,6 +84,8 @@
        private LiquibaseSettings liquibaseSettings;

     protected boolean isEnterpriseSession;
+
+    private ArchitectStatusInformation statusInfo;

        public ArchitectSessionImpl(final ArchitectSessionContext context,
                String name) throws SQLObjectException {
@@ -291,5 +293,14 @@
        public void setLiquibaseSettings(LiquibaseSettings settings) {
                liquibaseSettings = settings;
        }
+
+    @Override
+    public ArchitectStatusInformation getStatusInformation() {
+        return statusInfo;
+    }
+
+    public void setStatusInfo(ArchitectStatusInformation statusInfo) {
+        this.statusInfo = statusInfo;
+    }
 }

=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/enterprise/NetworkConflictResolver.java Fri Jul 16 08:41:08 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/enterprise/NetworkConflictResolver.java Tue Jul 27 14:11:45 2010
@@ -55,6 +55,7 @@
 import ca.sqlpower.dao.session.SessionPersisterSuperConverter;
 import ca.sqlpower.object.SPObject;
 import ca.sqlpower.sqlobject.SQLRelationship.ColumnMapping;
+import ca.sqlpower.util.MonitorableImpl;
 import ca.sqlpower.util.SQLPowerUtils;
 import ca.sqlpower.util.UserPrompterFactory;
 import ca.sqlpower.util.UserPrompter.UserPromptOptions;
@@ -75,6 +76,13 @@
      */
     private static final int MAX_CONFLICTS_TO_DISPLAY = 10;

+    /**
+ * This is currently a static average wait time for how long each change to + * the server will take. This will let the progress bar update at a decent
+     * rate.
+     */
+    private static final int AVG_WAIT_TIME_FOR_PERSIST = 12;
+
private static final Logger logger = Logger.getLogger(NetworkConflictResolver.class);
     private AtomicBoolean postingJSON = new AtomicBoolean(false);
     private boolean updating = false;
@@ -88,6 +96,14 @@
     private int currentRevision = 0;

     private long retryDelay = 1000;
+
+    /**
+ * This double will store and be updated with the average wait time for each + * persist calls so the progress of the progress bar is on average correct. + * At some point we may want the server to respond with the actual progress
+     * but this is a start.
+     */
+    private double currentWaitPerPersist = AVG_WAIT_TIME_FOR_PERSIST;

     private final SPJSONMessageDecoder jsonDecoder;

@@ -157,8 +173,36 @@
         if (postingJSON.get() && !reflush) {
             return;
         }
+        MonitorableImpl monitor = null;
+        long startTimeMillis = System.currentTimeMillis();
         try {
             postingJSON.set(true);
+
+ // Start a progress bar to update the user with the current changes.
+            if (session.getStatusInformation() != null) {
+ monitor = session.getStatusInformation().createProgressMonitor();
+                monitor.setJobSize(messageBuffer.length() + 2);
+ monitor.setMessage("Writing " + messageBuffer.length() + " changes to the server.");
+                monitor.setProgress(0);
+                final MonitorableImpl finalMonitor = monitor;
+                new Thread(new Runnable() {
+                    @Override
+                    public void run() {
+ while (finalMonitor.getProgress() < finalMonitor.getJobSize()) {
+                            try {
+                                Thread.sleep((long) currentWaitPerPersist);
+                            } catch (InterruptedException e) {
+                                throw new RuntimeException(e);
+                            }
+                            finalMonitor.incrementProgress();
+ finalMonitor.setMessage("Working on " + finalMonitor.getProgress() +
+                                    " of " + finalMonitor.getJobSize());
+                        }
+ finalMonitor.setMessage("Completing server update.");
+                    }
+                }).start();
+            }
+
             // Try to send json message ...
             JSONMessage response = null;
             try {
@@ -191,6 +235,9 @@
                 } catch (JSONException e) {
throw new RuntimeException("Could not update current revision" + e.getMessage());
                 }
+                long endTime = System.currentTimeMillis();
+ double processTimePerObj = (endTime - startTimeMillis) / messageBuffer.length(); + currentWaitPerPersist = currentWaitPerPersist * 0.9 + processTimePerObj * 0.1;
             } else {
// Did not successfully post json, we must update ourselves, and then try again if we can.
                 if (!reflush) {
@@ -264,6 +311,9 @@
                 }
             }
         } finally {
+            if (monitor != null) {
+                monitor.setFinished(true);
+            }
             postingJSON.set(false);
             clear(true);
         }
@@ -513,7 +563,7 @@
private Set<String> getColumnMappingChanges(Multimap<String, PersistedSPOProperty> properties) {
         Set<String> changes = new HashSet<String>();
         for (String uuid : properties.keySet()) {
-            Class type;
+            Class<?> type;
             String parentId;
             SPObject spo = session.getWorkspace().getObjectInTree(uuid);
             PersistedSPObject o = outboundObjectsToAdd.get(uuid);
=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectFrame.java Thu Jul 22 13:35:26 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectFrame.java Tue Jul 27 14:11:45 2010
@@ -256,6 +256,11 @@
     private CutSelectedAction cutAction;
     private PasteSelectedAction pasteAction;

+    /**
+     * A status bar that can have its content changed to useful messages.
+     */
+    private final ArchitectStatusBar statusBar = new ArchitectStatusBar();
+
     private RefreshProjectAction refreshProjectAction;

private List<SelectionListener> selectionListeners = new ArrayList<SelectionListener>();
@@ -838,6 +843,8 @@
         cp.add(splitPane, BorderLayout.CENTER);
         logger.debug("Added splitpane to content pane"); //$NON-NLS-1$

+        projectBarPane.add(statusBar.getStatusBar(), BorderLayout.SOUTH);
+
         stackedTabPane.setSelectedIndex(0);

         context.registerFrame(this);
@@ -1575,6 +1582,10 @@
     public EditCriticSettingsAction getShowCriticsManagerAction() {
         return showCriticsManagerAction;
     }
+
+    public ArchitectStatusBar getStatusBar() {
+        return statusBar;
+    }

     private class TabDropTargetListener implements DropTargetListener {

=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectSwingSession.java Mon Jul 12 08:21:11 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectSwingSession.java Tue Jul 27 14:11:45 2010
@@ -341,4 +341,6 @@
     public JScrollPane getPlayPenScrollPane();

     void setPlayPenScrollPane(JScrollPane ppScrollPane);
-}
+
+    ArchitectStatusBar getStatusInformation();
+}
=======================================
--- /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectSwingSessionImpl.java Thu Jul 22 13:35:26 2010 +++ /trunk/src/main/java/ca/sqlpower/architect/swingui/ArchitectSwingSessionImpl.java Tue Jul 27 14:11:45 2010
@@ -396,6 +396,8 @@
         }

         this.frame = parentFrame;
+
+ ((ArchitectSessionImpl)delegateSession).setStatusInfo(getStatusInformation());

         // makes the tool tips show up on these components
         ToolTipManager.sharedInstance().registerComponent(playPen);
@@ -1253,5 +1255,11 @@
     public void setProjectPanel(JComponent panel) {
         projectPanel = panel;
     }
+
+    @Override
+    public ArchitectStatusBar getStatusInformation() {
+        if (frame == null || frame.getStatusBar() == null) return null;
+        return frame.getStatusBar();
+    }

 }

Reply via email to