View Javadoc

1   /*
2    * Copyright (c) 2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.aequo.file;
14  
15  import java.io.File;
16  import java.io.FileFilter;
17  import java.io.IOException;
18  import java.util.ArrayList;
19  import java.util.Arrays;
20  import java.util.HashMap;
21  import java.util.HashSet;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.regex.Pattern;
27  
28  import javax.swing.DefaultListSelectionModel;
29  import javax.swing.ListSelectionModel;
30  import javax.swing.event.ListDataEvent;
31  import javax.swing.event.ListDataListener;
32  import javax.swing.event.TableModelEvent;
33  import javax.swing.event.TableModelListener;
34  import javax.swing.table.TableModel;
35  
36  import org.abstracthorizon.aequo.CompareEntry;
37  import org.abstracthorizon.aequo.CompareModel;
38  import org.abstracthorizon.aequo.util.filters.Filter;
39  
40  /**
41   * Files model.
42   *
43   * @author Daniel Sendula
44   */
45  public class FilesModel extends FileCompareEntry implements TableModel, CompareModel<File, FileCompareEntry> {
46  
47      /** Table change listeners */
48      protected ArrayList<TableModelListener> tableListeners = new ArrayList<TableModelListener>();
49  
50      /** List change listeners */
51      protected ArrayList<ListDataListener> listListeners = new ArrayList<ListDataListener>();
52  
53      /** Entry being processed (refreshed) listeners */
54      protected ArrayList<RefreshListener> refreshListeners = new ArrayList<RefreshListener>();
55  
56      /** All visible entries */
57      protected List<FileCompareEntry> visibleEntries = new ArrayList<FileCompareEntry>();
58  
59      /** Selection model */
60      protected DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
61  
62      /** Slow down for debug flag */
63      private static final boolean SLOWDOWN_FOR_DEBUG = false;
64  
65      /** Filters to be applied */
66      protected Filter filter;
67  
68      /** Shell model automatically expand folder where there are differences */
69      protected boolean expandDifferences = true;
70      /**
71       * Constructor
72       * @param leftDir left dir
73       * @param rightDir right dir
74       */
75      public FilesModel(File leftDir, File rightDir) {
76          super(null, new File[]{leftDir, rightDir});
77          filter = new Filter();
78      }
79  
80      /**
81       * Constructor
82       * @param leftDir left dir
83       * @param rightDir right dir
84       * @param filter filters to be applied
85       */
86      public FilesModel(File leftDir, File rightDir, Filter filter) {
87          super(null, new File[]{leftDir, rightDir});
88          this.filter = filter;
89      }
90  
91      /**
92       * Returns left dir
93       * @return left dir
94       */
95      public File getLeftFile() {
96          return data[0];
97      }
98  
99      /**
100      * Returns right dir
101      * @return right dir
102      */
103     public File getRightFile() {
104         return data[1];
105     }
106 
107     /**
108      * Returns filters
109      * @return filters
110      */
111     public Filter getFilter() {
112         return filter;
113     }
114 
115     /**
116      * Sets filters filters. Note: this won't really refresh model - you need to call {@link #refresh()} method.
117      * @param filter new filters
118      */
119     public void setFilter(Filter filter) {
120         this.filter = filter;
121     }
122 
123     /**
124      * Returns <code>true</code> if model will automatically expand differences
125      * @return <code>true</code> if model will automatically expand differences
126      */
127     public boolean isExpandDifferences() {
128         return expandDifferences;
129     }
130 
131     /**
132      * Sets if model will automatically expand differences
133      * @param expandDifferences should model automatically expand differences
134      */
135     public void setExpandDifferences(boolean expandDifferences) {
136         this.expandDifferences = expandDifferences;
137     }
138 
139     /**
140      * Reads the model from the given dirs
141      *
142      * @throws IOException
143      */
144     public void refresh() {
145         synchronized (visibleEntries) {
146             visibleEntries.clear();
147         }
148         refresh(this, 1, false);
149         if (children != null) {
150             for (FileCompareEntry e : children) {
151                 e.updateEntryStatus();
152             }
153             expand(this);
154         }
155         if (children != null) {
156             for (FileCompareEntry e : children) {
157                 refresh(e, 2, true);
158             }
159         }
160         notifyRefreshListeners(null);
161     }
162 
163     /**
164      * Reads given entry
165      * @param entry entry
166      * @param level level
167      * @param recursively shell it progress recursively
168      */
169     public void refresh(FileCompareEntry entry, int level, boolean recursively) {
170         refreshRecursively(entry, level, recursively);
171         if (expandDifferences && entry.hasChildren()) {
172             expandChangedRecursively(entry);
173         }
174     }
175 
176     /**
177      * Expands recursively all elements that have differences
178      *
179      * @param entry entry
180      */
181     protected void expandChangedRecursively(FileCompareEntry entry) {
182         int i  = 0;
183         boolean dontExpand = true;
184         while (dontExpand && (i < entry.children.length)) {
185             if (entry.children[i].getStatus(0) != CompareEntry.EQUAL) {
186                 dontExpand = false;
187             }
188             i++;
189         }
190         if (!dontExpand) {
191             expand(entry);
192         }
193         for (FileCompareEntry e : entry.children) {
194             if (e.hasChildren()) {
195                 expandChangedRecursively(e);
196             }
197         }
198     }
199 
200     /**
201      * Internal refresh method that goes through tree recursively (if <code>recursively</code>
202      * is set).
203      * @param entry entry
204      * @param level level
205      * @param recursively shell it progress recursively
206      */
207     protected void refreshRecursively(FileCompareEntry entry, int level, boolean recursively) {
208         boolean hasChanges = false;
209         Map<String, FileCompareEntry> entries = new HashMap<String, FileCompareEntry>();
210         if (entry.children != null) {
211             for (FileCompareEntry e : entry.children) {
212                 File[] files = e.getData();
213                 for (File f : files) {
214                     if (f != null) {
215                         entries.put(f.getName(), e);
216                         break;
217                     }
218                 }
219             }
220         }
221 
222         File[] files = entry.getData();
223         for (File dir : files) {
224             if (dir.exists() && dir.isDirectory()) {
225                 File[] fs = null;
226                 if (filter.isEmpty()) {
227                     fs = dir.listFiles();
228                 } else {
229                     fs = dir.listFiles(new FileFilter() {
230                         protected Pattern p = filter.getRegExpPattern();
231 
232                         public boolean accept(File pathname) {
233                             return !p.matcher(pathname.getName()).matches();
234                         }
235                     });
236                 }
237 
238                 if ((fs != null) && (fs.length > 0)) {
239                     for (File f : fs) {
240                         String name = f.getName();
241                         FileCompareEntry e = entries.get(name);
242                         if ((e == null) && (entry.children != null)) {
243                             for (int i = 0; (i < entry.children.length)
244                                     && (e == null); i++) {
245                                 if (name.equals(entry.children[i].getData(0)
246                                         .getName())) {
247                                     e = entry.children[i];
248                                 }
249                             }
250                         }
251                         if (e == null) {
252                             File[] nfs = new File[files.length];
253                             for (int i = 0; i < nfs.length; i++) {
254                                 nfs[i] = new File(files[i], name);
255                             }
256                             e = new FileCompareEntry(entry, nfs, level);
257                             // e.refreshStatus();
258                             hasChanges = true;
259                             entries.put(name, e);
260                         } else {
261                             boolean empty = true;
262                             int i = 0;
263                             File[] dfs = e.getData();
264                             while (empty && i < dfs.length) {
265                                 if (dfs[i].exists()) {
266                                     empty = false;
267                                 }
268                                 i++;
269                             }
270                             if (empty) {
271                                 entries.remove(e.getData(0).getName());
272                                 hasChanges = true;
273                             }
274                         }
275                     }
276                 }
277             }
278         }
279         Iterator<FileCompareEntry> it = entries.values().iterator();
280         while (it.hasNext()) {
281             FileCompareEntry e = it.next();
282             boolean notExist = true;
283             File[] fs = e.getData();
284             int i = 0;
285             while (notExist && (i < fs.length)) {
286                 if (fs[i].exists()) {
287                     notExist = false;
288                 }
289                 i++;
290             }
291             if (notExist) {
292                 it.remove();
293                 hasChanges = true;
294             }
295         }
296 
297         if (SLOWDOWN_FOR_DEBUG) {
298             try {
299                 Thread.sleep(50);
300             } catch (Exception ignore) {
301             }
302         }
303         if (hasChanges && entries.size() > 0) {
304             notifyRefreshListeners(entry);
305 
306             entry.children = new FileCompareEntry[entries.size()];
307 
308             Set<String> set = entries.keySet();
309             String[] names = new String[set.size()];
310             names = set.toArray(names);
311             Arrays.sort(names);
312             int i = 0;
313             for (String name : names) {
314                 entry.children[i] = entries.get(name);
315                 i++;
316             }
317             if (recursively) {
318                 for (FileCompareEntry e : entry.children) {
319                     refreshRecursively(e, level + 1, true);
320                     if (SLOWDOWN_FOR_DEBUG) {
321                         try {
322                             Thread.sleep(50);
323                         } catch (Exception ignore) {
324                         }
325                     }
326                 }
327             }
328         }
329         refreshVisible(entry, entries);
330         entry.updateEntryStatus();
331     }
332 
333     /**
334      * Filters out existing entries with set filter
335      */
336     public void filterOut() {
337         Pattern p = getFilter().getRegExpPattern();
338         filterOut(this, p);
339     }
340 
341     /**
342      * Filters out existing entries with given regular expression
343      * @param entry entry to start filtering out from
344      * @param p pattern compiled from regular expression
345      */
346     protected void filterOut(FileCompareEntry entry, Pattern p) {
347         FileCompareEntry[] children = entry.getChildren();
348         if ((children != null) && (children.length > 0)) {
349             int first = Integer.MAX_VALUE;
350             int last = -1;
351 
352             boolean removed = false;
353             ArrayList<FileCompareEntry> newChildren = new ArrayList<FileCompareEntry>(children.length);
354             ArrayList<FileCompareEntry> removedChildren = new ArrayList<FileCompareEntry>();
355             for (int i = 0; i < children.length; i++) {
356                 FileCompareEntry c = children[i];
357                 if (p.matcher(c.getData(0).getName()).matches()) {
358                     removed = true;
359                     if (c.index >= 0) {
360                         removedChildren.add(c);
361                         if (c.index < first) {
362                             first =  c.index;
363                         }
364                     }
365                 } else {
366                     newChildren.add(c);
367                 }
368             }
369             if (removed) {
370                 if (removedChildren.size() > 0) {
371                     FileCompareEntry lastEntry = removedChildren.get(removedChildren.size() - 1);
372 
373                     last = findMax(lastEntry);
374                 }
375 
376                 FileCompareEntry[] nc = new FileCompareEntry[newChildren.size()];
377                 nc = newChildren.toArray(nc);
378                 entry.children = nc;
379             }
380             children = entry.getChildren();
381             for (FileCompareEntry c : children) {
382                 if (c.hasChildren()) {
383                     filterOut(c, p);
384                 }
385             }
386             if (removed && (last >= 0)) {
387                 synchronized (visibleEntries) {
388                     for (int i = removedChildren.size() - 1; i >= 0; i--) {
389                         FileCompareEntry e  = removedChildren.get(i);
390                         if (e.index >= 0) {
391                             collapse(e);
392                             visibleEntries.remove(e.index);
393                         }
394                     }
395                 }
396                 renumber(entry);
397             }
398             fireChange(TableModelEvent.DELETE, first, last);
399         }
400     }
401 
402     /**
403      * Returns if entry is expanded or not
404      * @param entry entry
405      * @return <code>true</code> if entry is expanded
406      */
407     public boolean isExpanded(FileCompareEntry entry) {
408         return (entry.hasChildren() && entry.getChildren()[0].getIndex() >= 0);
409     }
410 
411     /**
412      * Returns if entry is collapsed or not
413      * @param entry entry
414      * @return <code>true</code> if entry is collapsed
415      */
416     public boolean isCollapsed(FileCompareEntry entry) {
417         return !entry.hasChildren() || (entry.hasChildren() && entry.getChildren()[0].getIndex() == -1);
418     }
419 
420     /**
421      * Refreshes visible entry for given entry
422      *
423      * @param entry
424      */
425     public void refreshVisible(FileCompareEntry entry, Map<String, FileCompareEntry> entries) {
426         if ((entry.getIndex() >= 0) || (entry.getLevel() == 0)) {
427             int firstChanged = -1;
428             int lastChanged = -1;
429 
430             int level = entry.getLevel() + 1;
431 
432             boolean added = false;
433             boolean removed = false;
434             FileCompareEntry[] children = entry.getChildren();
435 
436             int i = entry.getIndex() + 1;
437             int current = i;
438             int p = 0;
439 
440             boolean loop = true;
441             boolean changed;
442 
443             synchronized (visibleEntries) {
444                 while (loop) {
445                     changed = false;
446                     if (i < visibleEntries.size()) {
447                         FileCompareEntry e = visibleEntries.get(i);
448                         if (e.getLevel() == level) {
449                             if (!entries.containsValue(e)) {
450                                 visibleEntries.remove(i);
451                                 removed = true;
452                                 changed = true;
453                             } else {
454                                 FileCompareEntry f = children[p];
455                                 if (e != f) {
456                                     visibleEntries.add(i, f);
457                                     added = true;
458                                     changed = true;
459                                     i++;
460                                 } else {
461                                     // e == f
462                                     i++;
463                                 }
464                                 p++;
465                             }
466                         } else {
467                             if (children != null) {
468                                 // e.getLevel() != level
469                                 if (p >= children.length) {
470                                     loop = false;
471                                 } else {
472                                     FileCompareEntry f = children[p];
473                                     visibleEntries.add(i, f);
474                                     added = true;
475                                     changed = true;
476                                     i++;
477                                     p++;
478                                 }
479                             } else {
480                                 loop = false;
481                             }
482                         }
483                     } else {
484                         if ((children != null) && (p < children.length)) {
485                             FileCompareEntry e = children[p];
486                             visibleEntries.add(e);
487                             added = true;
488                             changed = true;
489                             p++;
490                             if (p >= children.length) {
491                                 loop = false;
492                             }
493                             i++;
494                         } else {
495                             loop = false;
496                         }
497                     }
498                     if (changed) {
499                         if (firstChanged < 0) {
500                             firstChanged = current;
501                         }
502                         lastChanged = current;
503                     }
504                     current++;
505                 } // loop
506                 if (added || removed) {
507                     renumber(entry);
508                 }
509             }
510             if (added && removed) {
511                 fireChange(TableModelEvent.UPDATE, firstChanged, lastChanged);
512             } else if (added) {
513                 fireChange(TableModelEvent.INSERT, firstChanged, lastChanged);
514             } else if (removed) {
515                 fireChange(TableModelEvent.DELETE, firstChanged, lastChanged);
516             }
517         }
518     }
519 
520     /**
521      * Expands given entry
522      * @param entry entry
523      */
524     public void expand(FileCompareEntry entry) {
525 
526         if (entry.children != null) {
527             synchronized (visibleEntries) {
528                 int i = entry.index + 1;
529                 int level = entry.getLevel() + 1;
530                 for (FileCompareEntry e : entry.children) {
531                     if (visibleEntries.size() > i) {
532                         if (visibleEntries.get(i) != e) {
533                             visibleEntries.add(i, e);
534                         }
535                     } else {
536                         visibleEntries.add(e);
537                     }
538                     i++;
539                 }
540                 if (visibleEntries.size() > i) {
541                     FileCompareEntry e = visibleEntries.get(i);
542                     while ((e != null) && (e.getLevel() == level)) {
543                         visibleEntries.remove(i);
544                         if (visibleEntries.size() > i) {
545                             e = visibleEntries.get(i);
546                         } else {
547                             e = null;
548                         }
549                     }
550                 }
551                 renumber(entry);
552             }
553             int first = entry.children[0].index;
554             int last = first + entry.children.length - 1;
555             fireChange(TableModelEvent.INSERT, first, last);
556         }
557     }
558 
559     /**
560      * Expands given entry and all sub-entries
561      * @param entry entry
562      */
563     public void expandAll(FileCompareEntry entry) {
564         if (entry.hasChildren()) {
565             expand(entry);
566             for (FileCompareEntry e : entry.getChildren()) {
567                 if (e.hasChildren()) {
568                     expandAll(e);
569                 }
570             }
571         }
572     }
573 
574     /**
575      * Collapses given entry
576      * @param entry entry
577      */
578     public void collapse(FileCompareEntry entry) {
579         if (entry.children != null) {
580             int first = entry.index + 1;
581             int last = 0;
582             boolean notifyChange = false;
583             synchronized (visibleEntries) {
584                 if (first < visibleEntries.size()) {
585                     int i = first;
586                     FileCompareEntry e = visibleEntries.get(i);
587                     while ((e != null) && (e.level > entry.level)) {
588                         e.index = -1;
589                         i++;
590                         if (i < visibleEntries.size()) {
591                             e = visibleEntries.get(i);
592                         } else {
593                             e = null;
594                         }
595                     }
596                     last = i - 1;
597                     for (i = last; i >= first; i--) {
598                         visibleEntries.remove(i);
599                     }
600                     renumber(entry);
601                     notifyChange = true;
602                 }
603             }
604             if (notifyChange) {
605                 fireChange(TableModelEvent.DELETE, first, last);
606             }
607         }
608     }
609 
610     /**
611      * Removes given entry
612      * @param entry entry
613      */
614     public void remove(FileCompareEntry entry) {
615         if (entry.index >= 0) {
616             collapse(entry);
617             int index = entry.index;
618             synchronized (visibleEntries) {
619                 visibleEntries.remove(entry.index);
620 
621                 FileCompareEntry parent = entry.parentEntry;
622                 if (parent != null) {
623                     if (parent.children.length == 1) {
624                         parent.children = null;
625                     } else {
626                         FileCompareEntry[] newChildren = new FileCompareEntry[parent.children.length - 1];
627                         int i = 0;
628                         int j = 0;
629                         while (i < parent.children.length) {
630                             if (entry == parent.children[i]) {
631                             } else {
632                                 newChildren[j] = parent.children[i];
633                                 j++;
634                             }
635                             i++;
636                         }
637                         parent.children = newChildren;
638                     }
639                     parent.updateEntryStatus();
640                 }
641 
642                 renumber(entry);
643                 entry.index = -1;
644             }
645             fireChange(TableModelEvent.DELETE, index, index);
646         }
647     }
648 
649     /**
650      * Finds maximum index in entry and its all children
651      * @param entry entry
652      * @return maximum index of entry or all visible children
653      */
654     protected int findMax(FileCompareEntry entry) {
655         if (entry.hasChildren()) {
656             FileCompareEntry[] children = entry.getChildren();
657             for (int i = children.length - 1; i >= 0; i--) {
658                 if (children[i].index >= 0) {
659                     return findMax(children[i]);
660                 }
661             }
662         }
663         return entry.index;
664     }
665 
666     /**
667      * Renumbers indexes of of entries from given entry
668      * @param entry entry to start renumbering from
669      */
670     protected void renumber(FileCompareEntry entry) {
671         for (int i = entry.index + 1; i < visibleEntries.size(); i++) {
672             visibleEntries.get(i).index = i;
673         }
674     }
675 
676     /**
677      * Adds refresh listener
678      *
679      * @param refreshListener listener
680      * @see #refreshListeners
681      */
682     public void addRefreshListener(RefreshListener refreshListener) {
683         refreshListeners.add(refreshListener);
684     }
685 
686     /**
687      * Removes refresh listener
688      * @param refreshListener listener
689      * @see #refreshListeners
690      */
691     public void removeRefreshListener(RefreshListener refreshListener) {
692         refreshListeners.remove(refreshListener);
693     }
694 
695     /**
696      * Notifies all refresh listeners
697      * @param entry entry that is being processes
698      */
699     protected void notifyRefreshListeners(FileCompareEntry entry) {
700         for (RefreshListener refreshListener : refreshListeners) {
701             refreshListener.currentlyProcessing(entry);
702         }
703     }
704 
705     /* CompareModel implementation */
706 
707     /**
708      * Returns entry at given index
709      * @return entry at given index
710      */
711     public FileCompareEntry get(int index) {
712         synchronized (visibleEntries) {
713             return visibleEntries.get(index);
714         }
715     }
716 
717     /**
718      * Returns selection model
719      * @return selection model
720      */
721     public ListSelectionModel getSelectionModel() {
722         return selectionModel;
723     }
724 
725     /**
726      * Adds list change listener
727      * @param l listener
728      */
729     public void addListDataListener(ListDataListener l) {
730         listListeners.add(l);
731     }
732 
733     /**
734      * Removes listener
735      * @param l listener
736      */
737     public void removeListDataListener(ListDataListener l) {
738         listListeners.remove(l);
739     }
740 
741     /**
742      * Returns entry at given index
743      * @return entry at given index
744      */
745     public synchronized Object getElementAt(int index) {
746         synchronized (visibleEntries) {
747             return visibleEntries.get(index);
748         }
749     }
750 
751     /**
752      * Returns number of entries
753      * @return number of entries
754      */
755     public synchronized int getSize() {
756         synchronized (visibleEntries) {
757             return visibleEntries.size();
758         }
759     }
760 
761     /* TableModel implementation */
762 
763     /**
764      * Adds table model listener
765      * @param l listener
766      */
767     public void addTableModelListener(TableModelListener l) {
768         tableListeners.add(l);
769     }
770 
771     /**
772      * Removes table model listener
773      * @param l listener
774      */
775     public void removeTableModelListener(TableModelListener l) {
776         tableListeners.remove(l);
777     }
778 
779     /**
780      * Returns column class
781      * @param columnIndex column index
782      * @return column class. It really returns {@link FileCompareEntry} in all cases then index is less then 3.
783      */
784     public Class<?> getColumnClass(int columnIndex) {
785         if (columnIndex < 3) {
786             return FileCompareEntry.class;
787         } else {
788             return null;
789         }
790     }
791 
792     /**
793      * Returns 3
794      * @return 3
795      */
796     public int getColumnCount() {
797         return 3;
798     }
799 
800     /**
801      * Returns column names
802      * @param columnIndex column index
803      */
804     public String getColumnName(int columnIndex) {
805         if (columnIndex == 0) {
806             return "Name";
807         } else if (columnIndex == 1) {
808             return "Size";
809         } else if (columnIndex == 2) {
810             return "Modified";
811         } else {
812             return null;
813         }
814     }
815 
816     /**
817      * Retruns row count - number of visible rows - number of elements in {@link #visibleEntries}.
818      * @return row count
819      */
820     public synchronized int getRowCount() {
821         synchronized (visibleEntries) {
822             return visibleEntries.size();
823         }
824     }
825 
826     /**
827      * Returns value at given row/column index
828      * @param rowIndex row index
829      * @param columnIndex column index
830      * @return value at given row/column index
831      */
832     public synchronized Object getValueAt(int rowIndex, int columnIndex) {
833         if (columnIndex < 3) {
834             synchronized (visibleEntries) {
835                 FileCompareEntry entry = (FileCompareEntry)visibleEntries.get(rowIndex);
836                 return entry;
837             }
838         } else {
839             return null;
840         }
841     }
842 
843     /**
844      * Returns <code>false</code>
845      * @param rowIndex row index
846      * @param columnIndex column index
847      * @return <code>false</code>
848      */
849     public boolean isCellEditable(int rowIndex, int columnIndex) {
850         return false;
851     }
852 
853     /**
854      * Does nothing
855      * @param value value
856      * @param rowIndex row index
857      * @param columnIndex column index
858      */
859     public void setValueAt(Object value, int rowIndex, int columnIndex) {
860     }
861 
862     /**
863      * Fires contents changed event for given entry
864      *
865      * @param entry entry
866      */
867     protected void contentsChanged(FileCompareEntry entry) {
868         fireChange(TableModelEvent.UPDATE, entry.index, entry.index); // TODO and all children if available
869     }
870 
871     /**
872      * Fires contents added event for given entry
873      *
874      * @param entry entry
875      */
876     protected void intervalAdded(FileCompareEntry entry) {
877         fireChange(TableModelEvent.INSERT, entry.index, entry.index); // TODO and all children if available
878     }
879 
880     /**
881      * Fires contents removed event for given entry
882      *
883      * @param entry entry
884      */
885     protected void intervalRemoved(FileCompareEntry entry) {
886         fireChange(TableModelEvent.DELETE, entry.index, entry.index); // TODO and all children if available
887     }
888 
889     /**
890      * Fires contents changed event for entries in given interval
891      * @param start starting index
892      * @param end ending index
893      */
894     public void notifyOfChange(int start, int end) {
895         fireChange(TableModelEvent.UPDATE, start, end);
896     }
897 
898     /**
899      * Fires contents removed event for entries in given interval
900      * @param start starting index
901      * @param end ending index
902      */
903     public void notifyOfRemoved(int start, int end) {
904         fireChange(TableModelEvent.DELETE, start, end);
905     }
906 
907     /**
908      * Fires contents changed inserted for entries in given interval
909      * @param start starting index
910      * @param end ending index
911      */
912     public void notifyOfInserted(int start, int end) {
913         fireChange(TableModelEvent.INSERT, start, end);
914     }
915 
916     /**
917      * Fires change of given type and interval
918      * @param type type of change as in {@link TableModelEvent#getType()}
919      * @param start starting index
920      * @param end ending index
921      */
922     protected void fireChange(int type, int start, int end) {
923         if (tableListeners.size() > 0) {
924             TableModelEvent event = new TableModelEvent(this, start, end, TableModelEvent.ALL_COLUMNS, type);
925             for (TableModelListener listener : tableListeners) {
926                 listener.tableChanged(event);
927             }
928         }
929 
930         if (listListeners.size() > 0) {
931             ListDataEvent event = null;
932             if (type == TableModelEvent.UPDATE) {
933                 event = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, start, end);
934             } else if (type == TableModelEvent.INSERT) {
935                 event = new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, start, end);
936             } else if (type == TableModelEvent.DELETE) {
937                 event = new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, start, end);
938             }
939             for (ListDataListener listener : listListeners) {
940                 if (type == TableModelEvent.UPDATE) {
941                     listener.contentsChanged(event);
942                 } else if (type == TableModelEvent.INSERT) {
943                     listener.intervalAdded(event);
944                 } else if (type == TableModelEvent.DELETE) {
945                     listener.intervalRemoved(event);
946                 }
947             }
948         }
949     }
950 
951     /**
952      * Method that is called by entries of the model notifying of change of the index. It delays
953      * before notifying listeners as these changes can be too frequent
954      *
955      * @param index index
956      */
957     protected void notifyEntryChanged(int index) {
958 //        notifyOfChange(index, index);
959         delayedEventThread.notifyOfChange(index);
960     }
961 
962     // TODO This thread must be stopped when window is disposed!!!
963     /** Thread to be used for delayed events */
964     protected DelayedEventThread delayedEventThread = new DelayedEventThread();
965 
966     /**
967      * Delayed event notifying thread.
968      */
969     protected class DelayedEventThread implements Runnable {
970 
971         /** Indexes of which listeners are to be notified of. */
972         protected Set<Integer> indexes = new HashSet<Integer>();
973 
974         /** Thread's state */
975         boolean triggered = false;
976 
977         /** Thread object itself */
978         protected Thread thread;
979 
980         /**
981          * Constructor. It creates and starts the daemon thread
982          */
983         public DelayedEventThread() {
984             thread = new Thread(this);
985             thread.setDaemon(true);
986             thread.start();
987         }
988 
989         /**
990          * Method to be called from outside of this object to trigger
991          * the notification cycle.
992          *
993          * @param index index to be passed to the listeners
994          */
995         public synchronized void notifyOfChange(int index) {
996             indexes.add(index);
997             if (!triggered) {
998                 triggered = true;
999                 notify();
1000             }
1001         }
1002 
1003         /**
1004          * Main method that has two states:
1005          * <ul>
1006          *   <li><b>triggered</b> - thread that waits for 300ms before notifying the listeners</li>
1007          *   <li><b>not triggered</b> - sleeping and waiting for the trigger</li>
1008          * </ul>
1009          *
1010          */
1011         public synchronized void run() {
1012             while (true) {
1013                 while (!triggered) {
1014                     try {
1015                         wait();
1016                     } catch (InterruptedException ignore) {
1017                     }
1018                 }
1019                 try {
1020                     wait(300);
1021                 } catch (InterruptedException ignore) {
1022                 }
1023                 for (int index : indexes) {
1024                     FilesModel.this.notifyOfChange(index, index);
1025                 }
1026                 indexes.clear();
1027                 triggered = false;
1028             }
1029         }
1030     }
1031 
1032     /**
1033      * Interface to be implemented by refresh listeners
1034      */
1035     public interface RefreshListener {
1036 
1037         /**
1038          * Invoked for each entry that is being processed. It is invoked only for
1039          * folders and not files
1040          * @param entry entry that is being processed
1041          */
1042         public void currentlyProcessing(FileCompareEntry entry);
1043     }
1044 }