EMMA Coverage Report (generated Sat Oct 08 11:41:37 CEST 2011)
[all classes][net.sf.jomic.ui]

COVERAGE SUMMARY FOR SOURCE FILE [JomicFrame.java]

nameclass, %method, %block, %line, %
JomicFrame.java71%  (5/7)46%  (41/89)43%  (1288/2982)45%  (303.5/670)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class JomicFrame$ExportWorker0%   (0/1)0%   (0/7)0%   (0/545)0%   (0/95)
<static initializer> 0%   (0/1)0%   (0/15)0%   (0/1)
JomicFrame$ExportWorker (JomicFrame, File): void 0%   (0/1)0%   (0/28)0%   (0/5)
askForOverwriteToBeCanceled (String [], int): boolean 0%   (0/1)0%   (0/129)0%   (0/22)
computeTotalSize (ExportItem []): long 0%   (0/1)0%   (0/29)0%   (0/6)
construct (): Object 0%   (0/1)0%   (0/280)0%   (0/46)
findExistingTargets (ExportItem []): String [] 0%   (0/1)0%   (0/54)0%   (0/12)
finished (): void 0%   (0/1)0%   (0/10)0%   (0/3)
     
class JomicFrame$OpenWorker$10%   (0/1)0%   (0/2)0%   (0/9)0%   (0/3)
JomicFrame$OpenWorker$1 (JomicFrame$OpenWorker): void 0%   (0/1)0%   (0/6)0%   (0/1)
run (): void 0%   (0/1)0%   (0/3)0%   (0/2)
     
class JomicFrame100% (1/1)43%  (30/69)49%  (863/1762)49%  (220.4/452)
JomicFrame (GraphicsConfiguration): void 0%   (0/1)0%   (0/9)0%   (0/4)
extractArchiveWithoutImages (File, String [], String): File 0%   (0/1)0%   (0/74)0%   (0/15)
fileToOpenFromArchiveWithoutImages (File, ComicMustContainImagesException): File 0%   (0/1)0%   (0/89)0%   (0/15)
mouseClicked (MouseEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
mouseDragged (MouseEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
mouseEntered (MouseEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
mouseExited (MouseEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
mouseMoved (MouseEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
mousePressed (MouseEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
mouseReleased (MouseEvent): void 0%   (0/1)0%   (0/73)0%   (0/17)
performAdance (): void 0%   (0/1)0%   (0/10)0%   (0/4)
performExport (): void 0%   (0/1)0%   (0/74)0%   (0/18)
performExportAll (): void 0%   (0/1)0%   (0/23)0%   (0/7)
performGoNext (): void 0%   (0/1)0%   (0/10)0%   (0/4)
performGoNextFew (): void 0%   (0/1)0%   (0/10)0%   (0/4)
performGoPage (): void 0%   (0/1)0%   (0/35)0%   (0/10)
performGoPrevious (): void 0%   (0/1)0%   (0/10)0%   (0/4)
performGoPreviousFew (): void 0%   (0/1)0%   (0/10)0%   (0/4)
performOpenRecent (ActionEvent): void 0%   (0/1)0%   (0/8)0%   (0/3)
performRetreat (): void 0%   (0/1)0%   (0/10)0%   (0/4)
performReveal (): void 0%   (0/1)0%   (0/6)0%   (0/2)
performScollRight (): void 0%   (0/1)0%   (0/5)0%   (0/2)
performScrollDown (): void 0%   (0/1)0%   (0/5)0%   (0/2)
performScrollLeft (): void 0%   (0/1)0%   (0/5)0%   (0/2)
performScrollUp (): void 0%   (0/1)0%   (0/5)0%   (0/2)
performToggelSwapLeftAndRightImage (): void 0%   (0/1)0%   (0/15)0%   (0/4)
performToggleRotateOnlySinglePortraitImages (): void 0%   (0/1)0%   (0/13)0%   (0/3)
performToggleShowInfo (): void 0%   (0/1)0%   (0/15)0%   (0/4)
performToggleShowThumbs (): void 0%   (0/1)0%   (0/13)0%   (0/3)
performToggleShowToolbar (): void 0%   (0/1)0%   (0/15)0%   (0/4)
performToggleShowTwoPages (): void 0%   (0/1)0%   (0/15)0%   (0/4)
prepareExporImageChooser (): void 0%   (0/1)0%   (0/46)0%   (0/11)
prepareExportAllImagesChooser (): void 0%   (0/1)0%   (0/46)0%   (0/11)
showCannotOpenError (File, Throwable): void 0%   (0/1)0%   (0/23)0%   (0/4)
valueChanged (ListSelectionEvent): void 0%   (0/1)0%   (0/34)0%   (0/10)
windowClosed (WindowEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
windowClosing (WindowEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
windowDeiconified (WindowEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
windowIconified (WindowEvent): void 0%   (0/1)0%   (0/1)0%   (0/1)
propertyChange (PropertyChangeEvent): void 100% (1/1)52%  (29/56)63%  (8.1/13)
waitForOpen (): void 100% (1/1)54%  (7/13)56%  (3.3/6)
actionPerformed (ActionEvent): void 100% (1/1)70%  (186/267)66%  (48.5/74)
performOpenNext (): void 100% (1/1)75%  (9/12)75%  (3/4)
performOpenPrevious (): void 100% (1/1)75%  (9/12)75%  (3/4)
addShortCutKey (InputMap, KeyStroke, String): void 100% (1/1)76%  (38/50)79%  (5.5/7)
rescale (String): void 100% (1/1)76%  (13/17)88%  (3.5/4)
<static initializer> 100% (1/1)78%  (28/36)89%  (2.7/3)
runOpen (File): SwingWorker 100% (1/1)81%  (17/21)90%  (4.5/5)
dispose (): void 100% (1/1)82%  (116/142)95%  (33.4/35)
refreshPageState (): void 100% (1/1)88%  (35/40)94%  (9.4/10)
assignShortCutKeys (InputMap): void 100% (1/1)95%  (71/75)96%  (12.5/13)
JomicFrame (): void 100% (1/1)100% (5/5)100% (3/3)
getComicModel (): ComicModel 100% (1/1)100% (3/3)100% (1/1)
getComicView (): ComicView 100% (1/1)100% (3/3)100% (1/1)
open (File): void 100% (1/1)100% (5/5)100% (2/2)
openSynchronously (File): void 100% (1/1)100% (8/8)100% (3/3)
performClose (): void 100% (1/1)100% (6/6)100% (2/2)
performGoLast (): void 100% (1/1)100% (6/6)100% (3/3)
performRotateLeft (): void 100% (1/1)100% (4/4)100% (2/2)
performRotateRight (): void 100% (1/1)100% (4/4)100% (2/2)
refreshTitle (): void 100% (1/1)100% (23/23)100% (3/3)
setUIState (String): void 100% (1/1)100% (11/11)100% (4/4)
setUp (): void 100% (1/1)100% (181/181)100% (43/43)
setUpShortCutKeys (): void 100% (1/1)100% (26/26)100% (6/6)
showError (String, Object, Throwable): void 100% (1/1)100% (8/8)100% (2/2)
showInternalCommandErrorMessage (String, Throwable): void 100% (1/1)100% (6/6)100% (2/2)
windowActivated (WindowEvent): void 100% (1/1)100% (4/4)100% (2/2)
windowDeactivated (WindowEvent): void 100% (1/1)100% (1/1)100% (1/1)
windowOpened (WindowEvent): void 100% (1/1)100% (1/1)100% (1/1)
     
class JomicFrame$OpenWorker100% (1/1)100% (5/5)61%  (371/607)66%  (70.7/107)
<static initializer> 100% (1/1)53%  (8/15)53%  (0.5/1)
construct (): Object 100% (1/1)57%  (240/419)61%  (43.5/71)
updatePreviousAndNextFile (): void 100% (1/1)67%  (68/102)74%  (14/19)
finished (): void 100% (1/1)76%  (37/49)73%  (8/11)
JomicFrame$OpenWorker (JomicFrame, File): void 100% (1/1)82%  (18/22)94%  (4.7/5)
     
class JomicFrame$OpenWorker$3100% (1/1)100% (2/2)86%  (32/37)91%  (6.4/7)
run (): void 100% (1/1)84%  (26/31)90%  (5.4/6)
JomicFrame$OpenWorker$3 (JomicFrame$OpenWorker): void 100% (1/1)100% (6/6)100% (1/1)
     
class JomicFrame$1100% (1/1)100% (2/2)100% (10/10)100% (3/3)
JomicFrame$1 (JomicFrame): void 100% (1/1)100% (6/6)100% (1/1)
run (): void 100% (1/1)100% (4/4)100% (2/2)
     
class JomicFrame$OpenWorker$2100% (1/1)100% (2/2)100% (12/12)100% (3/3)
JomicFrame$OpenWorker$2 (JomicFrame$OpenWorker): void 100% (1/1)100% (6/6)100% (1/1)
run (): void 100% (1/1)100% (6/6)100% (2/2)

1// Jomic - a viewer for comic book archives.
2// Copyright (C) 2004-2011 Thomas Aglassinger
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16package net.sf.jomic.ui;
17 
18import java.awt.BorderLayout;
19import java.awt.Container;
20import java.awt.GraphicsConfiguration;
21import java.awt.event.ActionEvent;
22import java.awt.event.ActionListener;
23import java.awt.event.InputEvent;
24import java.awt.event.MouseEvent;
25import java.awt.event.WindowEvent;
26import java.awt.event.WindowListener;
27import java.beans.PropertyChangeEvent;
28import java.beans.PropertyChangeListener;
29import java.io.File;
30import java.io.IOException;
31import java.util.Arrays;
32import java.util.Comparator;
33import java.util.LinkedList;
34import java.util.List;
35 
36import javax.swing.InputMap;
37import javax.swing.JComponent;
38import javax.swing.JFileChooser;
39import javax.swing.JFrame;
40import javax.swing.JLabel;
41import javax.swing.JOptionPane;
42import javax.swing.JPanel;
43import javax.swing.JScrollPane;
44import javax.swing.JTextArea;
45import javax.swing.KeyStroke;
46import javax.swing.ListSelectionModel;
47import javax.swing.SwingUtilities;
48import javax.swing.event.ListSelectionEvent;
49import javax.swing.event.ListSelectionListener;
50import javax.swing.event.MouseInputListener;
51 
52import net.sf.jomic.comic.ComicFileFilter;
53import net.sf.jomic.comic.ComicImage;
54import net.sf.jomic.comic.ComicInfoPanel;
55import net.sf.jomic.comic.ComicModel;
56import net.sf.jomic.comic.ComicMustContainImagesException;
57import net.sf.jomic.comic.ComicThumbView;
58import net.sf.jomic.comic.ComicView;
59import net.sf.jomic.common.ComicSheetRenderSettings;
60import net.sf.jomic.common.JomicTools;
61import net.sf.jomic.common.PropertyConstants;
62import net.sf.jomic.common.Settings;
63import net.sf.jomic.tools.FileArchive;
64import net.sf.jomic.tools.FileTools;
65import net.sf.jomic.tools.ImageRenderSettings;
66import net.sf.jomic.tools.ImageTools;
67import net.sf.jomic.tools.LocaleTools;
68import net.sf.jomic.tools.MutexLock;
69import net.sf.jomic.tools.NaturalCaseInsensitiveOrderComparator;
70import net.sf.jomic.tools.OperationCanceledException;
71import net.sf.jomic.tools.ProgressFrame;
72import net.sf.jomic.tools.StandardConstants;
73import net.sf.jomic.tools.StringTools;
74import net.sf.jomic.tools.SwingWorker;
75import net.sf.jomic.tools.SystemTools;
76import net.sf.jomic.tools.UiTools;
77 
78import org.apache.commons.logging.Log;
79import org.apache.commons.logging.LogFactory;
80 
81/**
82 *  JFrame containing the whole user interface acting as controller between the components.
83 *
84 * @author    Thomas Aglassinger
85 */
86public class JomicFrame extends JFrame
87         implements ActionListener, ListSelectionListener, MouseInputListener, PropertyChangeListener,
88        StandardConstants, WindowListener
89{
90    private static final int MILLIS_TO_IGNORE_LEFT_CLICK_AFTER_ACTIVATION = 100;
91 
92    private static final String[] PROPERTIES_TO_UPDATE_TITLE = new String[]{
93            ComicSheetRenderSettings.SWAP_LEFT_AND_RIGHT_IMAGE,
94            ComicSheetRenderSettings.SHOW_TWO_PAGES
95            };
96 
97    private static Log logger = LogFactory.getLog(JomicFrame.class);
98    private ComicModel comic;
99 
100    /**
101     *  The file containing the comic currently viewing, or null if no comic is loaded yet.
102     */
103    private File comicFile;
104    private ComicView comicView;
105    private boolean disposed;
106    private MutexLock disposedLock;
107    private JFileChooser exportAllImagesChooser;
108    private JFileChooser exportImageChooser;
109    private FileTools fileTools;
110    private boolean ignoreNextLeftMouseButtonReleased;
111    private ComicInfoPanel infoPane;
112    private JomicApplication jomicApplication;
113    private JomicTools jomicTools;
114 
115    /**
116     *  Timestamp when frame was last activated, allowing to ignore the first mouse click which
117     *  would change the current page.
118     *
119     * @see    #mouseReleased(MouseEvent)
120     */
121    private long lastActivatedMillis;
122    private LocaleTools localeTools;
123    private JomicMenuBar menuBar;
124    private boolean menuBarListenerAdded;
125    private boolean mouseListenerAdded;
126 
127    /**
128     *  The file that contains the next comic, or null, if there are no more comics in the directory
129     *  where the current comic is located.
130     */
131    private File nextFile;
132    private boolean opened;
133    private File previousFile;
134    private ProgressFrame progressFrame;
135    private boolean propertyChangeListenerAdded;
136    private Settings settings;
137    private StringTools stringTools;
138    private SystemTools systemTools;
139    private ComicThumbView thumbView;
140    private JomicToolbar toolbar;
141    private boolean toolbarListenerAdded;
142    private UiTools uiTools;
143 
144    public JomicFrame() {
145        setUp();
146    }
147 
148    public JomicFrame(GraphicsConfiguration gc) {
149        super(gc);
150        setUp();
151        setMenuBar(null);
152    }
153 
154    private void setUIState(String state) {
155        UIStates.assertValidState(state);
156        menuBar.setUIState(state);
157        toolbar.setUIState(state);
158    }
159 
160    private void setUp() {
161        fileTools = FileTools.instance();
162        jomicTools = JomicTools.instance();
163        localeTools = LocaleTools.instance();
164        systemTools = SystemTools.instance();
165        stringTools = StringTools.instance();
166        uiTools = UiTools.instance();
167 
168        disposedLock = new MutexLock("disposed");
169        settings = Settings.instance();
170        jomicApplication = JomicApplication.instance();
171 
172        // Build user interface.
173        menuBar = new JomicMenuBar();
174        setJMenuBar(menuBar);
175        toolbar = new JomicToolbar();
176        progressFrame = new ProgressFrame();
177        // TODO: (Re)Store position and size of progressFrame via settings.
178        uiTools.centerUp(progressFrame);
179 
180        jomicTools.setIconToJomicLogo(this);
181        jomicTools.setIconToJomicLogo(progressFrame);
182 
183        comicView = new ComicView();
184        comicView.setScaleMode(settings.getScaleMode());
185        comicView.setBorder(null);
186        comicView.getViewport().getView().addMouseListener(this);
187        mouseListenerAdded = true;
188 
189        addWindowListener(this);
190 
191        infoPane = new ComicInfoPanel();
192 
193        thumbView = new ComicThumbView();
194        thumbView.setBackground(settings.getFillColor());
195        thumbView.setOpaque(true);
196        thumbView.setVisible(settings.getShowThumbs());
197 
198        Container pane = getContentPane();
199 
200        pane.setLayout(new BorderLayout());
201        pane.add(toolbar, BorderLayout.NORTH);
202        pane.add(comicView, BorderLayout.CENTER);
203        pane.add(thumbView, BorderLayout.EAST);
204        pane.add(infoPane, BorderLayout.SOUTH);
205 
206        toolbar.addActionListener(this);
207        toolbarListenerAdded = true;
208        menuBar.addActionListener(this);
209        menuBarListenerAdded = true;
210        thumbView.addListSelectionListener(this);
211 
212        SwingUtilities.invokeLater(
213                    new Runnable()
214                    {
215                        public void run() {
216                            setUpShortCutKeys();
217                        }
218                    });
219        settings.addPropertyChangeListener(this);
220        propertyChangeListenerAdded = true;
221        infoPane.getTable().getSelectionModel().addListSelectionListener(this);
222    }
223 
224    private void setUpShortCutKeys() {
225        toolbar.getInputMap(JComponent.WHEN_FOCUSED).clear();
226        toolbar.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).clear();
227        toolbar.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).clear();
228        toolbar.getActionMap().clear();
229        assignShortCutKeys(comicView.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW));
230    }
231 
232    /**
233     *  The currenly opened comic, or <code>null</code> if no comic has been opened yet.
234     */
235    public ComicModel getComicModel() {
236        return comic;
237    }
238 
239    public ComicView getComicView() {
240        return comicView;
241    }
242 
243    /**
244     *  Asynchronously open comic <code>newFile</code> in viewer. During loading, the current comic
245     *  can still be viewed, but other file operations are disabled. A progress bar shows informs
246     *  about the status of loading the comic.
247     */
248    public final void open(File newFile) {
249        runOpen(newFile);
250    }
251 
252    /**
253     *  Synchronously open comic <code>newFile</code> in viewer. During loading, the current comic
254     *  can still be viewed, but other file operations are disabled. A progress bar shows informs
255     *  about the status of loading the comic.
256     */
257    public final void openSynchronously(File newFile) {
258        SwingWorker worker = runOpen(newFile);
259 
260        worker.get();
261    }
262 
263    public void actionPerformed(ActionEvent event) {
264        assert event != null;
265        String command = event.getActionCommand();
266 
267        try {
268            if (logger.isInfoEnabled()) {
269                logger.info("handle command: " + command);
270            }
271 
272            if (command.equals(Commands.ADVANCE)) {
273                performAdance();
274            } else if (command.equals(Commands.CLOSE)) {
275                performClose();
276            } else if (command.equals(Commands.EXPORT)) {
277                performExport();
278            } else if (command.equals(Commands.EXPORT_ALL)) {
279                performExportAll();
280            } else if (command.equals(Commands.FIRST_PAGE)) {
281                comicView.goFirst();
282                refreshPageState();
283            } else if (command.equals(Commands.FIT_ACTUAL_SIZE)) {
284                rescale(ImageTools.SCALE_ACTUAL);
285            } else if (command.equals(Commands.FIT_HEIGHT)) {
286                rescale(ImageTools.SCALE_HEIGHT);
287            } else if (command.equals(Commands.FIT_WIDTH)) {
288                rescale(ImageTools.SCALE_WIDTH);
289            } else if (command.equals(Commands.FIT_WIDTH_AND_HEIGHT)) {
290                rescale(ImageTools.SCALE_FIT);
291            } else if (command.equals(Commands.GO_NEXT)) {
292                performGoNext();
293            } else if (command.equals(Commands.GO_NEXT_FEW)) {
294                performGoNextFew();
295            } else if (command.equals(Commands.GO_PAGE)) {
296                performGoPage();
297            } else if (command.equals(Commands.GO_PREVIOUS)) {
298                performGoPrevious();
299            } else if (command.equals(Commands.GO_PREVIOUS_FEW)) {
300                performGoPreviousFew();
301            } else if (command.equals(Commands.LAST_PAGE)) {
302                performGoLast();
303            } else if (command.equals(Commands.OPEN_NEXT)) {
304                performOpenNext();
305            } else if (command.equals(Commands.OPEN_PREVIOUS)) {
306                performOpenPrevious();
307            } else if (command.equals(Commands.OPEN_RECENT)) {
308                performOpenRecent(event);
309            } else if (command.equals(Commands.RETREAT)) {
310                performRetreat();
311            } else if (command.equals(Commands.REVEAL)) {
312                performReveal();
313            } else if (command.equals(Commands.ROTATE_LEFT)) {
314                performRotateLeft();
315            } else if (command.equals(Commands.ROTATE_RIGHT)) {
316                performRotateRight();
317            } else if (command.equals(Commands.SCROLL_DOWN)) {
318                performScrollDown();
319            } else if (command.equals(Commands.SCROLL_LEFT)) {
320                performScrollLeft();
321            } else if (command.equals(Commands.SCROLL_RIGHT)) {
322                performScollRight();
323            } else if (command.equals(Commands.SCROLL_UP)) {
324                performScrollUp();
325            } else if (command.equals(Commands.TOGGLE_ROTATE_ONLY_SINGLE_PORTRAIT_IMAGES)) {
326                performToggleRotateOnlySinglePortraitImages();
327            } else if (command.equals(Commands.TOGGLE_SHOW_TWO_PAGES)) {
328                performToggleShowTwoPages();
329            } else if (command.equals(Commands.TOGGLE_SHOW_INFO)) {
330                performToggleShowInfo();
331            } else if (command.equals(Commands.TOGGLE_SHOW_THUMBS)) {
332                performToggleShowThumbs();
333            } else if (command.equals(Commands.TOGGLE_SWAP_LEFT_AND_RIGHT_IMAGE)) {
334                performToggelSwapLeftAndRightImage();
335            } else if (command.equals(Commands.TOGGLE_SHOW_TOOLBAR)) {
336                performToggleShowToolbar();
337            } else {
338                jomicApplication.actionPerformed(event);
339            }
340        } catch (Throwable error) {
341            showInternalCommandErrorMessage(command, error);
342        }
343    }
344 
345    public void dispose() {
346        if (logger.isDebugEnabled()) {
347            logger.debug("disposing " + JomicFrame.class,
348                    new Throwable("informational stack from where dispose() was called"));
349        }
350        synchronized (disposedLock) {
351            if (!disposed) {
352                removeWindowListener(this);
353                if (propertyChangeListenerAdded) {
354                    settings.removePropertyChangeListener(this);
355                }
356                if (infoPane != null) {
357                    infoPane.getTable().getSelectionModel().removeListSelectionListener(this);
358                    infoPane.dispose();
359                    infoPane = null;
360                }
361                if (comicView != null) {
362                    if (mouseListenerAdded) {
363                        comicView.getViewport().getView().removeMouseListener(this);
364                    }
365                    comicView.dispose();
366                    comicView = null;
367                }
368                if (comic != null) {
369                    comic.dispose();
370                    comic = null;
371                }
372                if (thumbView != null) {
373                    thumbView.removeListSelectionListener(this);
374                    thumbView.dispose();
375                    thumbView = null;
376                }
377                progressFrame.dispose();
378                if (toolbarListenerAdded) {
379                    toolbar.removeActionListener(this);
380                }
381                toolbar.dispose();
382                if (menuBarListenerAdded) {
383                    menuBar.removeActionListener(this);
384                }
385                menuBar.dispose();
386                super.dispose();
387                disposed = true;
388            } else {
389                logger.warn("ignored attempt to dispose frame again",
390                        new Throwable("informational call stack for debugging purpose"));
391            }
392        }
393    }
394 
395    public void mouseClicked(MouseEvent mouseEvent) {
396        // Do nothing.
397    }
398 
399    public void mouseDragged(MouseEvent mouseEvent) {
400        // Do nothing.
401    }
402 
403    public void mouseEntered(MouseEvent mouseEvent) {
404        // Do nothing.
405    }
406 
407    public void mouseExited(MouseEvent mouseEvent) {
408        // Do nothing.
409    }
410 
411    public void mouseMoved(MouseEvent mouseEvent) {
412        // Do nothing.
413    }
414 
415    public void mousePressed(MouseEvent mouseEvent) {
416        // Do nothing.
417    }
418 
419    public void mouseReleased(MouseEvent mouseEvent) {
420        if (SwingUtilities.isLeftMouseButton(mouseEvent)) {
421            long currentMillis = System.currentTimeMillis();
422            long millisSinceLastWindowActivation = currentMillis - lastActivatedMillis;
423            boolean ignoreFirstLeftClickAfterWindowActivation =
424                    millisSinceLastWindowActivation < MILLIS_TO_IGNORE_LEFT_CLICK_AFTER_ACTIVATION;
425 
426            if (ignoreFirstLeftClickAfterWindowActivation) {
427                if (logger.isInfoEnabled()) {
428                    logger.info("ignoring left mouse click because window was activated only "
429                            + millisSinceLastWindowActivation + " ms ago");
430                }
431                // Make sure the next click will be accepted.
432                lastActivatedMillis = currentMillis - MILLIS_TO_IGNORE_LEFT_CLICK_AFTER_ACTIVATION - 1;
433            } else if (ignoreNextLeftMouseButtonReleased) {
434                // Right mouse button was pressed with left mouse button down just before, so there
435                // is no point in going forward right now.
436                ignoreNextLeftMouseButtonReleased = false;
437            } else {
438                performAdance();
439            }
440        } else if (SwingUtilities.isRightMouseButton(mouseEvent)) {
441            boolean leftButtonDown = (mouseEvent.getModifiersEx() & InputEvent.BUTTON1_DOWN_MASK) != 0;
442 
443            if (leftButtonDown) {
444                performRetreat();
445                ignoreNextLeftMouseButtonReleased = true;
446            }
447        }
448    }
449 
450    public void propertyChange(PropertyChangeEvent event) {
451        try {
452            assert event != null;
453 
454            String propertyName = event.getPropertyName();
455 
456            assert propertyName != null;
457 
458            if (propertyName.equals(ImageRenderSettings.FILL_COLOR)) {
459                thumbView.setBackground(settings.getFillColor());
460            } else if (propertyName.equals(PropertyConstants.SHOW_THUMBS)) {
461                thumbView.setVisible(settings.getShowThumbs());
462            }
463            if (stringTools.equalsAnyOf(PROPERTIES_TO_UPDATE_TITLE, propertyName)) {
464                refreshTitle();
465            }
466        } catch (Throwable error) {
467            jomicTools.showError(event, error);
468        }
469    }
470 
471    /**
472     *  Listen for selection events in the info panel, and view selected page.
473     *
474     * @see    javax.swing.event.ListSelectionListener#valueChanged(javax.swing.event.ListSelectionEvent)
475     */
476    public void valueChanged(ListSelectionEvent event) {
477        if (!event.getValueIsAdjusting()) {
478            ListSelectionModel lsm =
479                    (ListSelectionModel) event.getSource();
480 
481            if (!lsm.isSelectionEmpty()) {
482                int selectedRow = lsm.getMinSelectionIndex();
483 
484                comicView.setImageIndex(selectedRow);
485                refreshPageState();
486            }
487        } else {
488            if (logger.isDebugEnabled()) {
489                logger.debug("ignore valueChanged: " + event);
490            }
491        }
492    }
493 
494    /**
495     *  Wait until <code>open()</code> is finished opening the comic (or failed to open it). This is
496     *  useful for testing.
497     */
498    public void waitForOpen() {
499        while (!opened) {
500            try {
501                Thread.sleep(TICK);
502            } catch (InterruptedException error) {
503                logger.warn("waiting for open() interrupted; continuing to wait", error);
504            }
505        }
506    }
507 
508    public void windowActivated(WindowEvent windowEvent) {
509        lastActivatedMillis = System.currentTimeMillis();
510    }
511 
512    public void windowClosed(WindowEvent windowEvent) {
513        // Do nothing.
514    }
515 
516    public void windowClosing(WindowEvent windowEvent) {
517        // Do nothing.
518    }
519 
520    public void windowDeactivated(WindowEvent windowEvent) {
521        // Do nothing.
522    }
523 
524    public void windowDeiconified(WindowEvent windowEvent) {
525        // Do nothing.
526    }
527 
528    public void windowIconified(WindowEvent windowEvent) {
529        // Do nothing.
530    }
531 
532    public void windowOpened(WindowEvent windowEvent) {
533        // Do nothing.
534    }
535 
536    void performClose() {
537        SwingUtilities.invokeLater(new JomicFrameCloseRunner(this));
538    }
539 
540    /**
541     *  Prepare <code>exportImageChooser</code> by possibly creating it first, and setting the
542     *  selected file name to the most recently exported image.
543     */
544    void prepareExporImageChooser() {
545        // Create the dialog if necessary.
546        if (exportImageChooser == null) {
547            String title = localeTools.getMessage("dialogs.export.title");
548            String exportButtonText = localeTools.getMessage("dialogs.export.exportImage");
549 
550            exportImageChooser = new SnapableJFileChooser(
551                    settings, PropertyConstants.EXPORT_IMAGE_DIALOG_WINDOW);
552            exportImageChooser.setDialogTitle(title);
553            exportImageChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
554            exportImageChooser.setDialogType(JFileChooser.SAVE_DIALOG);
555            exportImageChooser.setApproveButtonText(exportButtonText);
556 
557            // Set the selected file to the one used for the last export.
558            File lastExportmage = settings.getLastExportedImageDir();
559 
560            exportImageChooser.setSelectedFile(lastExportmage);
561        }
562    }
563 
564    /**
565     *  Prepare <code>exportAllImagesChooser</code> by possibly creating it first, and setting the
566     *  selected file name to the most recently exported directory.
567     */
568    void prepareExportAllImagesChooser() {
569        // Create the dialog if necessary.
570        if (exportAllImagesChooser == null) {
571            String title = localeTools.getMessage("dialogs.exportAllImages.title");
572            String exportButtonText = localeTools.getMessage("dialogs.exportAllImages.exportAllImages");
573 
574            exportAllImagesChooser = new SnapableJFileChooser(
575                    settings, PropertyConstants.EXPORT_ALL_IMAGES_DIALOG_WINDOW);
576            exportAllImagesChooser.setDialogTitle(title);
577            exportAllImagesChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
578            exportAllImagesChooser.setDialogType(JFileChooser.SAVE_DIALOG);
579            exportAllImagesChooser.setApproveButtonText(exportButtonText);
580 
581            // Set the selected directory to the one used for the last export.
582            File lastExportAllImagesDir = settings.getLastExportAllImagesDir();
583 
584            exportAllImagesChooser.setSelectedFile(lastExportAllImagesDir);
585        }
586    }
587 
588    private void addShortCutKey(InputMap inputMap, KeyStroke keyStroke, String command) {
589        assert inputMap != null;
590        assert keyStroke != null;
591        assert command != null;
592        uiTools.removeKeyStroke(toolbar, new KeyStroke[]{keyStroke});
593        inputMap.put(keyStroke, command);
594        comicView.getActionMap().put(command, new JomicAction(this, command));
595    }
596 
597    private void assignShortCutKeys(InputMap inputMap) {
598        assert inputMap != null;
599        addShortCutKey(inputMap, KeyStroke.getKeyStroke("DOWN"), Commands.SCROLL_DOWN);
600        addShortCutKey(inputMap, KeyStroke.getKeyStroke("END"), Commands.LAST_PAGE);
601        addShortCutKey(inputMap, KeyStroke.getKeyStroke("HOME"), Commands.FIRST_PAGE);
602        addShortCutKey(inputMap, KeyStroke.getKeyStroke("LEFT"), Commands.SCROLL_LEFT);
603        addShortCutKey(inputMap, KeyStroke.getKeyStroke("PAGE_DOWN"), Commands.GO_NEXT);
604        addShortCutKey(inputMap, KeyStroke.getKeyStroke("PAGE_UP"), Commands.GO_PREVIOUS);
605        addShortCutKey(inputMap, KeyStroke.getKeyStroke("alt PAGE_DOWN"), Commands.GO_NEXT_FEW);
606        addShortCutKey(inputMap, KeyStroke.getKeyStroke("alt PAGE_UP"), Commands.GO_PREVIOUS_FEW);
607        addShortCutKey(inputMap, KeyStroke.getKeyStroke("RIGHT"), Commands.SCROLL_RIGHT);
608        addShortCutKey(inputMap, KeyStroke.getKeyStroke("SPACE"), Commands.ADVANCE);
609        addShortCutKey(inputMap, KeyStroke.getKeyStroke("UP"), Commands.SCROLL_UP);
610    }
611 
612    /**
613     *  Extract from archive <code>archiveFile</code> all files listed in <code>fileNamesToExtract</code>
614     *  , and return the full path to the extracted <code>selectedFileName</code>. Normally, this
615     *  indicates the comic selected in the dialog opened by the caller.
616     */
617    private File extractArchiveWithoutImages(
618            File archiveFile,
619            String[] fileNamesToExtract,
620            String selectedFileName) {
621        assert archiveFile != null;
622        assert fileNamesToExtract != null;
623        assert fileNamesToExtract.length > 0;
624 
625        File result = null;
626 
627        try {
628            FileArchive archive = new FileArchive(archiveFile);
629            File targetDir = new File(archiveFile.getParent(), archive.getBaseName());
630 
631            if (selectedFileName != null) {
632                result = new File(targetDir, selectedFileName);
633            }
634            targetDir.mkdir();
635            progressFrame.setTitle(archiveFile.getName());
636            archive.extract(targetDir, fileNamesToExtract, progressFrame, 1);
637        } catch (Exception error) {
638            jomicTools.showError(this, "errors.cannotExtractArchive", archiveFile, error);
639        }
640        return result;
641    }
642 
643    private File fileToOpenFromArchiveWithoutImages(
644            File archiveWithoutImagesFile, ComicMustContainImagesException error) {
645        File result = null;
646        OpenFromArchiveDialog openFromArchiveOptionPane =
647                new OpenFromArchiveDialog(this, archiveWithoutImagesFile, error);
648 
649        int selection = openFromArchiveOptionPane.showDialog();
650 
651        if (selection == JOptionPane.YES_OPTION) {
652 
653            if (openFromArchiveOptionPane.isOpenComic()) {
654                String comicArchiveName =
655                        openFromArchiveOptionPane.getFileNames()[openFromArchiveOptionPane.getSelectedComicIndex()];
656 
657                logger.info("extract contents of archive without images, and open comic: " + comicArchiveName);
658                result = extractArchiveWithoutImages(
659                        archiveWithoutImagesFile,
660                        openFromArchiveOptionPane.getComicFileNames(),
661                        comicArchiveName);
662                logger.info("extracted comic = \"" + result + "\"");
663            } else {
664                logger.info("extract contents of archive without images");
665                extractArchiveWithoutImages(archiveWithoutImagesFile, openFromArchiveOptionPane.getFileNames(), null);
666            }
667        } else {
668            assert (selection == JOptionPane.NO_OPTION) || (selection == JOptionPane.CLOSED_OPTION)
669                    : "selection=" + selection;
670            logger.info("cancelled opening of comic from archive");
671        }
672        return result;
673    }
674 
675    private void performAdance() {
676        if (!comicView.isLast()) {
677            performGoNext();
678        } else {
679            performOpenNext();
680        }
681    }
682 
683    /**
684     *  Open a file chooser to let the user select a target image file and on clicking "Export"
685     *  export it.
686     */
687    private void performExport() {
688        prepareExporImageChooser();
689 
690        // Derive default name of exported image, and preselected it in the dialog.
691        ComicImage comicImage = comicView.getComicImage();
692        File sourceImageFile = comicImage.getFile();
693        String preselectedName = sourceImageFile.getName();
694        File preselectedDir = settings.getLastExportedImageDir();
695        File preselectedFile = new File(preselectedDir, preselectedName);
696 
697        exportImageChooser.setSelectedFile(preselectedFile);
698 
699        int selection = exportImageChooser.showDialog(this, null);
700 
701        if (selection == JFileChooser.APPROVE_OPTION) {
702            File targetImageFile = exportImageChooser.getSelectedFile();
703 
704            try {
705                setUIState(UIStates.OPENING);
706                fileTools.copyFile(sourceImageFile, targetImageFile);
707                settings.setLastExportedImageDir(targetImageFile.getParentFile());
708            } catch (IOException error) {
709                jomicTools.showError(this, "errors.cannotExportCurrentImage", targetImageFile, error);
710            } finally {
711                setUIState(UIStates.VIEWING);
712            }
713        }
714    }
715 
716    /**
717     *  Open a file chooser to let the user select a target directory, then export all images to it,
718     *  storing them under files names matching the display index of the image (for example
719     *  "17.png").
720     */
721    private void performExportAll() {
722        prepareExportAllImagesChooser();
723 
724        int selection = exportAllImagesChooser.showDialog(this, null);
725 
726        if (selection == JFileChooser.APPROVE_OPTION) {
727            File targetDir = exportAllImagesChooser.getSelectedFile();
728            SwingWorker exportWorker = new ExportWorker(targetDir);
729 
730            exportWorker.start();
731        }
732    }
733 
734    private void performGoLast() {
735        comicView.goLast();
736        refreshPageState();
737    }
738 
739    private void performGoNext() {
740        if (!comicView.isLast()) {
741            comicView.goNext();
742            refreshPageState();
743        }
744    }
745 
746    private void performGoNextFew() {
747        if (!comicView.isLast()) {
748            comicView.goNextFew();
749            refreshPageState();
750        }
751    }
752 
753    private void performGoPage() {
754        if (!(comicView.isFirst() && comicView.isLast())) {
755            GoToPageDialog pageDialog = new GoToPageDialog(this);
756 
757            pageDialog.setVisible(true);
758            if (pageDialog.pageSelected()) {
759                int newPage = pageDialog.getPage() - 1;
760 
761                comicView.setPage(newPage);
762                refreshPageState();
763            } else {
764                logger.info("page dialog cancelled");
765            }
766        }
767    }
768 
769    private void performGoPrevious() {
770        if (!comicView.isFirst()) {
771            comicView.goPrevious();
772            refreshPageState();
773        }
774    }
775 
776    private void performGoPreviousFew() {
777        if (!comicView.isFirst()) {
778            comicView.goPreviousFew();
779            refreshPageState();
780        }
781    }
782 
783    private void performOpenNext() {
784        if (nextFile != null) {
785            open(nextFile);
786        } else {
787            jomicTools.beep();
788        }
789    }
790 
791    private void performOpenPrevious() {
792        if (previousFile != null) {
793            open(previousFile);
794        } else {
795            jomicTools.beep();
796        }
797    }
798 
799    private void performOpenRecent(ActionEvent event) {
800        OpenRecentFileEvent orfEvent = (OpenRecentFileEvent) event;
801 
802        open(orfEvent.getFile());
803    }
804 
805    private void performRetreat() {
806        if (!comicView.isFirst()) {
807            performGoPrevious();
808        } else {
809            performOpenPrevious();
810        }
811    }
812 
813    private void performReveal()
814        throws IOException, InterruptedException {
815        systemTools.reveal(comicFile);
816    }
817 
818    private void performRotateLeft() {
819        comicView.rotateLeft();
820    }
821 
822    private void performRotateRight() {
823        comicView.rotateRight();
824    }
825 
826    private void performScollRight() {
827        comicView.scrollHorizontally(1);
828    }
829 
830    private void performScrollDown() {
831        comicView.scrollVertically(1);
832    }
833 
834    private void performScrollLeft() {
835        comicView.scrollHorizontally(-1);
836    }
837 
838    private void performScrollUp() {
839        comicView.scrollVertically(-1);
840    }
841 
842    private void performToggelSwapLeftAndRightImage() {
843        boolean newValue = !settings.getSwapLeftAndRightImage();
844 
845        comicView.setSwapLeftAndRightImage(newValue);
846        refreshTitle();
847    }
848 
849    private void performToggleRotateOnlySinglePortraitImages() {
850        boolean newValue = !settings.getRotateOnlySinglePortraitImages();
851 
852        comicView.setRotateOnlySinglePortraitImages(newValue);
853    }
854 
855    private void performToggleShowInfo() {
856        boolean newShowInfo = !settings.getShowInfo();
857 
858        settings.setShowInfo(newShowInfo);
859        validate();
860    }
861 
862    private void performToggleShowThumbs() {
863        settings.setShowThumbs(!settings.getShowThumbs());
864        validate();
865    }
866 
867    private void performToggleShowToolbar() {
868        boolean newValue = !settings.getShowToolbar();
869 
870        settings.setShowToolbar(newValue);
871        validate();
872    }
873 
874    private void performToggleShowTwoPages() {
875        boolean newValue = !settings.getTwoPageMode();
876 
877        comicView.setTwoPageMode(newValue);
878        refreshTitle();
879    }
880 
881    private void refreshPageState() {
882        synchronized (disposedLock) {
883            if (comicView != null) {
884                boolean isFirst = comicView.isFirst();
885                boolean isLast = comicView.isLast();
886 
887                menuBar.setEnabledGoMenu(isFirst, isLast);
888                toolbar.setEnabledPageButtons(isFirst, isLast);
889                comicView.scrollHome();
890                refreshTitle();
891            }
892        }
893    }
894 
895    private void refreshTitle() {
896        String title = localeTools.getMessage("panels.comic.title",
897                new String[]{comicFile.getName(), comicView.getPageText()});
898 
899        setTitle(title);
900    }
901 
902    private void rescale(String newScaleMode) {
903        assert newScaleMode != null;
904 
905        comicView.setScaleMode(newScaleMode);
906        settings.setScaleMode(newScaleMode);
907    }
908 
909    /**
910     *  Asynchronously open comic <code>newFile</code> in viewer. During loading, the current comic
911     *  can still be viewed, but other file operations are disabled. A progress bar shows informs
912     *  about the status of loading the comic.
913     */
914    private SwingWorker runOpen(File newFile) {
915        assert newFile != null;
916        setUIState(UIStates.OPENING);
917 
918        SwingWorker worker = new OpenWorker(newFile);
919 
920        worker.start();
921 
922        return worker;
923    }
924 
925    private void showCannotOpenError(File comicFileUnableToOpen, Throwable error) {
926        assert comicFile != null;
927        assert error != null;
928 
929        showError("errors.cannotOpenComic", comicFileUnableToOpen, error);
930    }
931 
932    private void showError(String key, Object option, Throwable error) {
933        jomicTools.showError(this, key, option, error);
934    }
935 
936    private void showInternalCommandErrorMessage(String command, Throwable error) {
937        showError("errors.cannotProcessInternalCommand", command, error);
938    }
939 
940    /**
941     *  SwingWorker to open a comic archive outside of the event dispatching thread, so the progress
942     *  bar is updated.
943     */
944    class ExportWorker extends SwingWorker
945    {
946        private static final int MAX_OVERWRITE_LIST_COLUMNS = 60;
947        private static final int MAX_OVERWRITE_LIST_ROWS = 7;
948        private boolean canceled;
949        private Log exportLogger = LogFactory.getLog(ExportWorker.class);
950        private int maxExistingTargetNameLength;
951        private File targetDir;
952 
953        public ExportWorker(File newTargetDir) {
954            assert newTargetDir != null;
955            targetDir = newTargetDir;
956        }
957 
958        public Object construct() {
959            int itemCount = comicView.getImageCount();
960 
961            setUIState(UIStates.OPENING);
962            progressFrame.reset();
963            progressFrame.setTitle(localeTools.getMessage("dialogs.export.title"));
964            progressFrame.setNote(localeTools.getMessage("progress.preparingExport"));
965            progressFrame.setVisible(true);
966 
967            ExportItem[] items = new ExportItem[itemCount];
968 
969            for (int i = 0; i < itemCount; i += 1) {
970                ComicImage comicImage = comicView.getComicImage(i);
971                File source = comicImage.getFile();
972                File target = new File(targetDir, comicView.getGenericComicImageName(i));
973                ExportItem item = new ExportItem(source, target);
974 
975                items[i] = item;
976            }
977 
978            String[] existingTargets = findExistingTargets(items);
979            long totalSize = computeTotalSize(items);
980 
981            if (logger.isInfoEnabled()) {
982                logger.info("export " + itemCount + " images amounting for " + (totalSize / KILO_BYTE) + " KB");
983            }
984 
985            int existingTargetCount = existingTargets.length;
986 
987            if (existingTargetCount > 0) {
988                canceled = askForOverwriteToBeCanceled(existingTargets, existingTargetCount);
989            }
990            if (!canceled) {
991                long bytesExported = 0;
992 
993                progressFrame.setMaximum(totalSize);
994                progressFrame.setProgress(0);
995                progressFrame.setNote(localeTools.getMessage("progress.exporting"));
996 
997                int processedCount = 0;
998 
999                try {
1000                    while (!canceled && (processedCount < itemCount)) {
1001                        if (progressFrame.isCanceled()) {
1002                            canceled = true;
1003                        } else {
1004                            ExportItem item = items[processedCount];
1005 
1006                            fileTools.copyFile(item.getSource(), item.getTarget());
1007                            bytesExported += item.getSize();
1008                            progressFrame.setProgress(bytesExported);
1009                            processedCount += 1;
1010                        }
1011                    }
1012                } catch (Exception error) {
1013                    String key = "errors.cannotExportImage";
1014 
1015                    if (itemCount > 1) {
1016                        key += "s";
1017                    }
1018                    canceled = true;
1019                    progressFrame.setVisible(false);
1020                    showError(key, null, error);
1021                } finally {
1022                    if (canceled) {
1023                        // Remove already exported images.
1024                        for (int i = 0; i < processedCount; i += 1) {
1025                            fileTools.deleteOrWarn(items[i].getTarget(), exportLogger);
1026                        }
1027                    }
1028                }
1029            }
1030            return null;
1031        }
1032 
1033        public void finished() {
1034            progressFrame.setVisible(false);
1035            setUIState(UIStates.VIEWING);
1036        }
1037 
1038        /**
1039         *  Show dialog with a list of file names to be overwritten, and aks if they actually should
1040         *  be.
1041         *
1042         * @return    <code>true</code> to overwrite, <code>false</code> to cancel export
1043         */
1044        private boolean askForOverwriteToBeCanceled(String[] existingTargets, int existingTargetCount) {
1045            int columnCount = Math.min(MAX_OVERWRITE_LIST_COLUMNS, maxExistingTargetNameLength);
1046            int rowCount = Math.min(MAX_OVERWRITE_LIST_ROWS, existingTargetCount);
1047            JTextArea fileNameArea = new JTextArea(rowCount, columnCount);
1048 
1049            fileNameArea.setEditable(false);
1050 
1051            for (int i = 0; i < existingTargetCount; i += 1) {
1052                if (i != 0) {
1053                    fileNameArea.append("\n");
1054                }
1055                fileNameArea.append(existingTargets[i]);
1056            }
1057 
1058            JScrollPane fileNamePane = new JScrollPane(fileNameArea);
1059            JPanel messagePane = new JPanel(new BorderLayout());
1060            String infoText = localeTools.getMessage("dialogs.export.imagesToBeExportedAlreadyExist");
1061            String title = localeTools.getMessage("dialogs.export.title");
1062            String overwriteText = localeTools.getMessage("dialogs.export.overwrite");
1063            String cancelText = localeTools.getCancelText();
1064            Object[] options = {overwriteText, cancelText};
1065 
1066            messagePane.add(new JLabel(infoText), BorderLayout.NORTH);
1067            messagePane.add(fileNamePane, BorderLayout.SOUTH);
1068            progressFrame.setVisible(false);
1069 
1070            int selectedOption = JOptionPane.showOptionDialog(
1071                    null, messagePane, title,
1072                    JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE,
1073                    null, options, options[0]);
1074 
1075            canceled = (selectedOption == 0);
1076            progressFrame.setVisible(!canceled);
1077            return canceled;
1078        }
1079 
1080        private long computeTotalSize(ExportItem[] items) {
1081            assert items != null;
1082            long result = 0;
1083 
1084            for (int i = 0; i < items.length; i += 1) {
1085                ExportItem item = items[i];
1086 
1087                result += item.getSize();
1088            }
1089            return result;
1090        }
1091 
1092        private String[] findExistingTargets(ExportItem[] items) {
1093            assert items != null;
1094            List result = new LinkedList();
1095 
1096            for (int i = 0; i < items.length; i += 1) {
1097                ExportItem item = items[i];
1098 
1099                File target = item.getTarget();
1100 
1101                if (target.exists()) {
1102                    String existingTargetName = target.getAbsolutePath();
1103                    int existingTargetNameLength = existingTargetName.length();
1104 
1105                    result.add(existingTargetName);
1106                    if (existingTargetNameLength > maxExistingTargetNameLength) {
1107                        maxExistingTargetNameLength = existingTargetNameLength;
1108                    }
1109                }
1110            }
1111            return (String[]) result.toArray(new String[0]);
1112        }
1113    }
1114 
1115    /**
1116     *  SwingWorker to open a comic archive outside of the event dispatching thread, so the progress
1117     *  bar is updated.
1118     */
1119    class OpenWorker extends SwingWorker
1120    {
1121        private boolean comicOpened;
1122        private File oldFile;
1123 
1124        public OpenWorker(File newFile) {
1125            assert newFile != null;
1126            oldFile = comicFile;
1127            comicFile = newFile;
1128        }
1129 
1130        public Object construct() {
1131            try {
1132                if (logger.isInfoEnabled()) {
1133                    logger.info("open file \"" + comicFile + "\"");
1134                }
1135                synchronized (disposedLock) {
1136                    if (!disposed) {
1137                        // Dispose old comic only after new comic could be read.
1138                        boolean disposeOldComic = false;
1139                        boolean retry;
1140 
1141                        ComicModel oldComic = comic;
1142 
1143                        do {
1144                            retry = false;
1145                            try {
1146                                progressFrame.reset();
1147                                progressFrame.setTitle(comicFile.getName());
1148                                progressFrame.setNote(".");
1149                                progressFrame.setVisible(true);
1150                                comic = new ComicModel(comicFile, progressFrame);
1151 
1152                                int pageToOpen;
1153 
1154                                if (comicFile.equals(settings.getMostRecentFile())) {
1155                                    pageToOpen = settings.getMostRecentPage();
1156                                } else {
1157                                    pageToOpen = 0;
1158                                }
1159                                comicView.setModel(comic, progressFrame, pageToOpen);
1160                                setTitle(comicFile.getName() + " - Jomic");
1161                                // FIXME: Don't remove old comic files if we just reopened the same comic again.
1162                                disposeOldComic = true;
1163                                infoPane.setComicModel(comic);
1164                                comicOpened = true;
1165                            } catch (ComicMustContainImagesException error) {
1166                                if (error.getFileNames().length > 0) {
1167                                    comicFile = fileToOpenFromArchiveWithoutImages(comicFile, error);
1168                                    retry = (comicFile != null);
1169                                } else {
1170                                    throw error;
1171                                }
1172                            } catch (Exception error) {
1173                                // TODO: Restore comic model and page.
1174                                comicOpened = false;
1175                                progressFrame.setVisible(false);
1176                                if (error instanceof OperationCanceledException) {
1177                                    logger.warn("opening of comic canceled");
1178                                } else {
1179                                    showCannotOpenError(comicFile, error);
1180                                }
1181                            }
1182                            if (retry) {
1183                                if (logger.isInfoEnabled()) {
1184                                    logger.info("retrying with \"" + comicFile + "\"");
1185                                }
1186                            }
1187                        } while (retry);
1188 
1189                        if (retry) {
1190                            assert comicFile != null : "retry=" + retry + ", file=\"" + comicFile + "\"";
1191                        }
1192                        if (comicOpened) {
1193                            assert comicFile != null : "comicOpened=" + comicOpened + ", file=\"" + comicFile + "\"";
1194                        }
1195 
1196                        // TODO: Figure out if there is a need to check for (file != null).
1197                        if (disposeOldComic && (oldComic != null) && !comicFile.equals(oldFile)) {
1198                            try {
1199                                oldComic.dispose();
1200                            } catch (Exception error) {
1201                                jomicTools.showWarning(null, "warnings.cannotCleanupTemporaryFiles", error);
1202                            }
1203                        }
1204                        if (comicOpened) {
1205                            if (settings.getAdjustArchiveSuffix()) {
1206                                File adjustedComicFile = fileTools.getAdjustedComicFile(comicFile);
1207 
1208                                if (!comicFile.equals(adjustedComicFile)) {
1209                                    boolean renamed = comicFile.renameTo(adjustedComicFile);
1210 
1211                                    if (renamed) {
1212                                        comicFile = adjustedComicFile;
1213                                    }
1214                                }
1215                            }
1216                            updatePreviousAndNextFile();
1217                            settings.addRecentFile(comicFile, comicView.getPage());
1218                            thumbView.setComic(comic);
1219                        } else {
1220                            comicFile = oldFile;
1221                        }
1222                        menuBar.updateFileOpenRecent();
1223                        if (comic == null) {
1224                            setUIState(UIStates.EMPTY);
1225                        } else {
1226                            setUIState(UIStates.VIEWING);
1227                            if (comicOpened && settings.getOpenInFullScreen()) {
1228                                SwingUtilities.invokeLater(
1229                                            new Runnable()
1230                                            {
1231                                                public void run() {
1232                                                    JomicApplication.instance().performToggleFullScreen();
1233                                                }
1234                                            });
1235                            }
1236                        }
1237                        menuBar.setEnabledOpenPreviousAndNext(previousFile != null, nextFile != null);
1238                    } else {
1239                        logger.warn("ignored open because frame was already disposed",
1240                                new Throwable("stack for debugging"));
1241                    }
1242                }
1243            } catch (Throwable error) {
1244                progressFrame.setVisible(false);
1245                showCannotOpenError(comicFile, error);
1246            }
1247            return null;
1248        }
1249 
1250        public void finished() {
1251            // Note: this runs on the event-dispatching thread.
1252            if (comicOpened) {
1253                if (!isVisible() && !settings.getOpenInFullScreen()) {
1254                    SwingUtilities.invokeLater(
1255                                new Runnable()
1256                                {
1257                                    public void run() {
1258                                        setVisible(true);
1259                                    }
1260                                });
1261                }
1262                SwingUtilities.invokeLater(
1263                            new Runnable()
1264                            {
1265                                public void run() {
1266                                    synchronized (disposedLock) {
1267                                        if (comicView != null) {
1268                                            comicView.validate();
1269                                            comicView.updateDisplay();
1270                                        }
1271                                    }
1272                                }
1273                            });
1274                refreshPageState();
1275                progressFrame.setVisible(false);
1276                opened = true;
1277            } else if (comicFile == null) {
1278                opened = true;
1279                performClose();
1280            }
1281        }
1282 
1283        /**
1284         *  Set nextFile to next file in directory of current comic file, or <code>null</code> if
1285         *  there are no more files.
1286         */
1287        private void updatePreviousAndNextFile() {
1288            File fileDir = comicFile.getParentFile();
1289            String fileName = comicFile.getName();
1290            String[] comicFiles = fileDir.list(new ComicFileFilter());
1291            Comparator naturalOrderComparator = new NaturalCaseInsensitiveOrderComparator();
1292 
1293            Arrays.sort(comicFiles, naturalOrderComparator);
1294 
1295            int fileIndex = Arrays.binarySearch(comicFiles, fileName, naturalOrderComparator);
1296 
1297            if (fileIndex >= 0) {
1298                int nextFileIndex = fileIndex + 1;
1299 
1300                if ((nextFileIndex) < comicFiles.length) {
1301                    nextFile = new File(fileDir, comicFiles[nextFileIndex]);
1302                } else {
1303                    nextFile = null;
1304                }
1305                if (fileIndex > 0) {
1306                    previousFile = new File(fileDir, comicFiles[fileIndex - 1]);
1307                } else {
1308                    previousFile = null;
1309                }
1310            } else {
1311                // Comic is a file with an unrecognized suffix.
1312                logger.warn("comic file suffix not recognized; cannot figure out a previous or next comic: "
1313                        + stringTools.sourced(fileName));
1314                previousFile = null;
1315                nextFile = null;
1316            }
1317        }
1318    }
1319}

[all classes][net.sf.jomic.ui]
EMMA 2.0.4217 (C) Vladimir Roubtsov