EMMA Coverage Report (generated Sun Apr 20 22:38:01 CEST 2008)
[all classes][net.sf.jomic.ui]

COVERAGE SUMMARY FOR SOURCE FILE [JomicFrame.java]

nameclass, %method, %block, %line, %
JomicFrame.java50%  (5/10)45%  (43/95)43%  (1289/3029)44%  (301/677)

COVERAGE BREAKDOWN BY CLASS AND METHOD

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