mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 12:18:00 +00:00
document and clean up migration dialog code
This commit is contained in:
@@ -29,6 +29,7 @@ import java.util.HashMap;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
import java.util.concurrent.ConcurrentSkipListMap;
|
import java.util.concurrent.ConcurrentSkipListMap;
|
||||||
|
|
||||||
import javax.swing.JComboBox;
|
import javax.swing.JComboBox;
|
||||||
@@ -64,12 +65,18 @@ import forge.gui.toolbox.FSkin;
|
|||||||
import forge.gui.toolbox.FTextField;
|
import forge.gui.toolbox.FTextField;
|
||||||
import forge.properties.NewConstants;
|
import forge.properties.NewConstants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements an overlay-based dialog that imports data from a user-selected directory
|
||||||
|
* into the correct locations in the user and cache directories. There is a lot of I/O and data
|
||||||
|
* processing done in this class, so most operations are asynchronous.
|
||||||
|
*/
|
||||||
public class DialogMigrateProfile {
|
public class DialogMigrateProfile {
|
||||||
private final Runnable _onImportSuccessful;
|
private final Runnable _onImportSuccessful;
|
||||||
private final FButton _btnStart;
|
private final FButton _btnStart;
|
||||||
private final FLabel _btnChooseDir;
|
private final FLabel _btnChooseDir;
|
||||||
private final JPanel _selectionPanel;
|
private final JPanel _selectionPanel;
|
||||||
|
|
||||||
|
// volatile since it is checked from multiple threads
|
||||||
private volatile boolean _cancel;
|
private volatile boolean _cancel;
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
@@ -83,6 +90,7 @@ public class DialogMigrateProfile {
|
|||||||
// header
|
// header
|
||||||
p.add(new FLabel.Builder().text((isMigration ? "Migrate" : "Import") + " profile data").fontSize(15).build(), "center");
|
p.add(new FLabel.Builder().text((isMigration ? "Migrate" : "Import") + " profile data").fontSize(15).build(), "center");
|
||||||
|
|
||||||
|
// add some help text if this is for the initial data migration
|
||||||
if (isMigration) {
|
if (isMigration) {
|
||||||
FPanel blurbPanel = new FPanel(new MigLayout("insets dialog, gap 10, center, wrap"));
|
FPanel blurbPanel = new FPanel(new MigLayout("insets dialog, gap 10, center, wrap"));
|
||||||
blurbPanel.setOpaque(false);
|
blurbPanel.setOpaque(false);
|
||||||
@@ -124,12 +132,18 @@ public class DialogMigrateProfile {
|
|||||||
_fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
_fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||||||
_btnChooseDir.setCommand(new Command() {
|
_btnChooseDir.setCommand(new Command() {
|
||||||
@Override public void execute() {
|
@Override public void execute() {
|
||||||
|
// bring up a file open dialog and, if the OK button is selected, apply the filename
|
||||||
|
// to the import source text field
|
||||||
if (JFileChooser.APPROVE_OPTION == _fileChooser.showOpenDialog(null)) {
|
if (JFileChooser.APPROVE_OPTION == _fileChooser.showOpenDialog(null)) {
|
||||||
txfSrc.setText(_fileChooser.getSelectedFile().getAbsolutePath());
|
txfSrc.setText(_fileChooser.getSelectedFile().getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
importSourcePanel.add(_btnChooseDir, "h pref+8!, w pref+12!");
|
importSourcePanel.add(_btnChooseDir, "h pref+8!, w pref+12!");
|
||||||
|
|
||||||
|
// add change handler to the import source text field that starts up a
|
||||||
|
// new analyzer. it also interacts with the current active analyzer,
|
||||||
|
// if any, to make sure it cancels out before the new one is initiated
|
||||||
txfSrc.getDocument().addDocumentListener(new DocumentListener() {
|
txfSrc.getDocument().addDocumentListener(new DocumentListener() {
|
||||||
boolean _analyzerActive; // access synchronized on _onAnalyzerDone
|
boolean _analyzerActive; // access synchronized on _onAnalyzerDone
|
||||||
String prevText;
|
String prevText;
|
||||||
@@ -144,8 +158,11 @@ public class DialogMigrateProfile {
|
|||||||
@Override public void removeUpdate(DocumentEvent e) { }
|
@Override public void removeUpdate(DocumentEvent e) { }
|
||||||
@Override public void changedUpdate(DocumentEvent e) { }
|
@Override public void changedUpdate(DocumentEvent e) { }
|
||||||
@Override public void insertUpdate(DocumentEvent e) {
|
@Override public void insertUpdate(DocumentEvent e) {
|
||||||
|
// text field is read-only, so the only time this will get updated
|
||||||
|
// is when _btnChooseDir does it
|
||||||
final String text = txfSrc.getText();
|
final String text = txfSrc.getText();
|
||||||
if (text.equals(prevText)) {
|
if (text.equals(prevText)) {
|
||||||
|
// only restart the analyzer if the directory has changed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prevText = text;
|
prevText = text;
|
||||||
@@ -154,9 +171,14 @@ public class DialogMigrateProfile {
|
|||||||
_cancel = true;
|
_cancel = true;
|
||||||
|
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
|
// ensure we don't get two instances of this function running at the same time
|
||||||
_btnChooseDir.setEnabled(false);
|
_btnChooseDir.setEnabled(false);
|
||||||
|
|
||||||
|
// re-disable the start button. it will be enabled if the previous analyzer has
|
||||||
|
// already successfully finished
|
||||||
_btnStart.setEnabled(false);
|
_btnStart.setEnabled(false);
|
||||||
|
|
||||||
|
// we have to wait in a background thread since we can't block in the GUI thread
|
||||||
SwingWorker<Void, Void> analyzerStarter = new SwingWorker<Void, Void>() {
|
SwingWorker<Void, Void> analyzerStarter = new SwingWorker<Void, Void>() {
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground() throws Exception {
|
protected Void doInBackground() throws Exception {
|
||||||
@@ -166,7 +188,6 @@ public class DialogMigrateProfile {
|
|||||||
_onAnalyzerDone.wait();
|
_onAnalyzerDone.wait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,11 +196,14 @@ public class DialogMigrateProfile {
|
|||||||
protected void done() {
|
protected void done() {
|
||||||
_cancel = false;
|
_cancel = false;
|
||||||
synchronized (_onAnalyzerDone) {
|
synchronized (_onAnalyzerDone) {
|
||||||
|
// this will populate the panel with data selection widgets
|
||||||
_AnalyzerUpdater analyzer = new _AnalyzerUpdater(text, _onAnalyzerDone);
|
_AnalyzerUpdater analyzer = new _AnalyzerUpdater(text, _onAnalyzerDone);
|
||||||
analyzer.execute();
|
analyzer.execute();
|
||||||
_analyzerActive = true;
|
_analyzerActive = true;
|
||||||
}
|
}
|
||||||
if (!isMigration) {
|
if (!isMigration) {
|
||||||
|
// only enable the directory choosing button if this is not a migration dialog
|
||||||
|
// since in that case we're permanently locked to the starting directory
|
||||||
_btnChooseDir.setEnabled(true);
|
_btnChooseDir.setEnabled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +214,7 @@ public class DialogMigrateProfile {
|
|||||||
});
|
});
|
||||||
p.add(importSourcePanel, "gaptop 20, pushx, growx");
|
p.add(importSourcePanel, "gaptop 20, pushx, growx");
|
||||||
|
|
||||||
// prepare import selection panel
|
// prepare import selection panel (will be cleared and filled in later by an analyzer)
|
||||||
_selectionPanel = new JPanel();
|
_selectionPanel = new JPanel();
|
||||||
_selectionPanel.setOpaque(false);
|
_selectionPanel.setOpaque(false);
|
||||||
p.add(_selectionPanel, "growx, h 100%, gaptop 10");
|
p.add(_selectionPanel, "growx, h 100%, gaptop 10");
|
||||||
@@ -201,7 +225,6 @@ public class DialogMigrateProfile {
|
|||||||
};
|
};
|
||||||
_btnStart = new FButton("Start import");
|
_btnStart = new FButton("Start import");
|
||||||
_btnStart.setEnabled(false);
|
_btnStart.setEnabled(false);
|
||||||
|
|
||||||
final FButton btnCancel = new FButton("Cancel");
|
final FButton btnCancel = new FButton("Cancel");
|
||||||
btnCancel.addActionListener(new ActionListener() {
|
btnCancel.addActionListener(new ActionListener() {
|
||||||
@Override public void actionPerformed(ActionEvent e) {
|
@Override public void actionPerformed(ActionEvent e) {
|
||||||
@@ -223,25 +246,27 @@ public class DialogMigrateProfile {
|
|||||||
southPanel.setOpaque(false);
|
southPanel.setOpaque(false);
|
||||||
southPanel.add(_btnStart, "center, w pref+72!, h pref+12!");
|
southPanel.add(_btnStart, "center, w pref+72!, h pref+12!");
|
||||||
southPanel.add(btnCancel, "center, w pref+72!, h pref+12!");
|
southPanel.add(btnCancel, "center, w pref+72!, h pref+12!");
|
||||||
|
|
||||||
p.add(southPanel, "growx");
|
p.add(southPanel, "growx");
|
||||||
|
|
||||||
JPanel overlay = FOverlay.SINGLETON_INSTANCE.getPanel();
|
JPanel overlay = FOverlay.SINGLETON_INSTANCE.getPanel();
|
||||||
overlay.setLayout(new MigLayout("insets 0, gap 0, wrap, ax center, ay center"));
|
overlay.setLayout(new MigLayout("insets 0, gap 0, wrap, ax center, ay center"));
|
||||||
overlay.add(p, "w 400::80%, h 100::90%");
|
overlay.add(p, "w 500::80%, h 100::90%");
|
||||||
SOverlayUtils.showOverlay();
|
SOverlayUtils.showOverlay();
|
||||||
|
|
||||||
// focus cancel button
|
// focus cancel button after the dialog is shown
|
||||||
SwingUtilities.invokeLater(new Runnable() {
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
@Override public void run() { btnCancel.requestFocusInWindow(); }
|
@Override public void run() { btnCancel.requestFocusInWindow(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// if our source dir is provided, set the text, which will fire off an analyzer
|
||||||
if (isMigration) {
|
if (isMigration) {
|
||||||
File srcDirFile = new File(forcedSrcDir);
|
File srcDirFile = new File(forcedSrcDir);
|
||||||
txfSrc.setText(srcDirFile.getAbsolutePath());
|
txfSrc.setText(srcDirFile.getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encapsulates the choices in the combobox for choosing the destination paths for
|
||||||
|
// decks of unknown type
|
||||||
private class _UnknownDeckChoice {
|
private class _UnknownDeckChoice {
|
||||||
public final String name;
|
public final String name;
|
||||||
public final String path;
|
public final String path;
|
||||||
@@ -251,14 +276,18 @@ public class DialogMigrateProfile {
|
|||||||
path = path0;
|
path = path0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override public String toString() { return name; }
|
||||||
public String toString() { return name; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this class owns the import selection widgets and bridges them with the running
|
||||||
|
// MigrationSourceAnalyzer instance
|
||||||
private class _AnalyzerUpdater extends SwingWorker<Void, Void> {
|
private class _AnalyzerUpdater extends SwingWorker<Void, Void> {
|
||||||
|
// associates a file operation type with its enablement checkbox and the set
|
||||||
|
// of file move/copy operations that enabling it would entail
|
||||||
private final Map<OpType, Pair<FCheckBox, ? extends Map<File, File>>> _selections =
|
private final Map<OpType, Pair<FCheckBox, ? extends Map<File, File>>> _selections =
|
||||||
new HashMap<OpType, Pair<FCheckBox, ? extends Map<File, File>>>();
|
new HashMap<OpType, Pair<FCheckBox, ? extends Map<File, File>>>();
|
||||||
|
|
||||||
|
// attached to all changeable widgets to keep the UI in sync
|
||||||
private final ChangeListener _stateChangedListener = new ChangeListener() {
|
private final ChangeListener _stateChangedListener = new ChangeListener() {
|
||||||
@Override public void stateChanged(ChangeEvent arg0) { _updateUI(); }
|
@Override public void stateChanged(ChangeEvent arg0) { _updateUI(); }
|
||||||
};
|
};
|
||||||
@@ -271,6 +300,7 @@ public class DialogMigrateProfile {
|
|||||||
private final JTextArea _operationLog;
|
private final JTextArea _operationLog;
|
||||||
private final JProgressBar _progressBar;
|
private final JProgressBar _progressBar;
|
||||||
|
|
||||||
|
// updates the _operationLog widget asynchronously to keep the UI responsive
|
||||||
private final _OperationLogAsyncUpdater _operationLogUpdater;
|
private final _OperationLogAsyncUpdater _operationLogUpdater;
|
||||||
|
|
||||||
public _AnalyzerUpdater(String srcDir, Runnable onAnalyzerDone) {
|
public _AnalyzerUpdater(String srcDir, Runnable onAnalyzerDone) {
|
||||||
@@ -362,10 +392,11 @@ public class DialogMigrateProfile {
|
|||||||
_progressBar.setStringPainted(true);
|
_progressBar.setStringPainted(true);
|
||||||
_selectionPanel.add(_progressBar, "w 100%!");
|
_selectionPanel.add(_progressBar, "w 100%!");
|
||||||
|
|
||||||
|
// start the op log updater
|
||||||
_operationLogUpdater = new _OperationLogAsyncUpdater(_selections, _operationLog);
|
_operationLogUpdater = new _OperationLogAsyncUpdater(_selections, _operationLog);
|
||||||
_operationLogUpdater.start();
|
_operationLogUpdater.start();
|
||||||
|
|
||||||
// set checkbox labels
|
// set initial checkbox labels
|
||||||
_updateUI();
|
_updateUI();
|
||||||
|
|
||||||
// resize the panel properly now that the _selectionPanel is filled in
|
// resize the panel properly now that the _selectionPanel is filled in
|
||||||
@@ -379,14 +410,16 @@ public class DialogMigrateProfile {
|
|||||||
cb.setSelected(true);
|
cb.setSelected(true);
|
||||||
cb.addChangeListener(_stateChangedListener);
|
cb.addChangeListener(_stateChangedListener);
|
||||||
|
|
||||||
// use a skip list map instead of a regular hashmap so that the files are sorted alphabetically
|
// use a skip list map instead of a regular hashmap so that the files are sorted
|
||||||
|
// alphabetically in the logs. note that this is a concurrent data structure
|
||||||
|
// since it will be modified and read simultaneously by different threads
|
||||||
_selections.put(type, Pair.of(cb, new ConcurrentSkipListMap<File, File>()));
|
_selections.put(type, Pair.of(cb, new ConcurrentSkipListMap<File, File>()));
|
||||||
parent.add(cb);
|
parent.add(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// must be called from GUI event loop thread
|
// must be called from GUI event loop thread
|
||||||
private void _updateUI() {
|
private void _updateUI() {
|
||||||
// update checkbox text lables with current totals
|
// update checkbox text labels with current totals
|
||||||
Set<OpType> selectedOptions = new HashSet<OpType>();
|
Set<OpType> selectedOptions = new HashSet<OpType>();
|
||||||
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : _selections.entrySet()) {
|
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : _selections.entrySet()) {
|
||||||
Pair<FCheckBox, ? extends Map<File, File>> selection = entry.getValue();
|
Pair<FCheckBox, ? extends Map<File, File>> selection = entry.getValue();
|
||||||
@@ -399,19 +432,12 @@ public class DialogMigrateProfile {
|
|||||||
cb.setText(String.format("%s (%d)", cb.getName(), selection.getRight().size()));
|
cb.setText(String.format("%s (%d)", cb.getName(), selection.getRight().size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// asynchronously update the text in the op log, which may be many tens of thousands of lines long
|
||||||
|
// if this were done synchronously the UI would slow to a crawl
|
||||||
_operationLogUpdater.requestUpdate(selectedOptions, (_UnknownDeckChoice)_unknownDeckCombo.getSelectedItem(),
|
_operationLogUpdater.requestUpdate(selectedOptions, (_UnknownDeckChoice)_unknownDeckCombo.getSelectedItem(),
|
||||||
_moveCheckbox.isSelected(), _overwriteCheckbox.isSelected());
|
_moveCheckbox.isSelected(), _overwriteCheckbox.isSelected());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _disableAll() {
|
|
||||||
for (Pair<FCheckBox, ? extends Map<File, File>> selection : _selections.values()) {
|
|
||||||
selection.getLeft().setEnabled(false);
|
|
||||||
}
|
|
||||||
_unknownDeckCombo.setEnabled(false);
|
|
||||||
_moveCheckbox.setEnabled(false);
|
|
||||||
_overwriteCheckbox.setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground() throws Exception {
|
protected Void doInBackground() throws Exception {
|
||||||
Timer timer = null;
|
Timer timer = null;
|
||||||
@@ -450,6 +476,7 @@ public class DialogMigrateProfile {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update the progress bar widget from the GUI event loop
|
||||||
SwingUtilities.invokeLater(new Runnable() {
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
if (_cancel) { return; }
|
if (_cancel) { return; }
|
||||||
@@ -463,6 +490,7 @@ public class DialogMigrateProfile {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// does not return until analysis is complete or has been canceled
|
||||||
msa.doAnalysis();
|
msa.doAnalysis();
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
_cancel = true;
|
_cancel = true;
|
||||||
@@ -474,8 +502,8 @@ public class DialogMigrateProfile {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (null != timer)
|
// ensure the UI update timer is stopped after analysis is complete
|
||||||
{
|
if (null != timer) {
|
||||||
timer.stop();
|
timer.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,28 +519,49 @@ public class DialogMigrateProfile {
|
|||||||
_updateUI();
|
_updateUI();
|
||||||
_progressBar.setString("Analysis complete");
|
_progressBar.setString("Analysis complete");
|
||||||
|
|
||||||
|
// clear any previously-set action listeners on the start button
|
||||||
|
// in case we've previously completed an analysis but changed the directory
|
||||||
|
// instead of starting the import
|
||||||
|
for (ActionListener a : _btnStart.getActionListeners()) {
|
||||||
|
_btnStart.removeActionListener(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up the start button to start the prepared import on click
|
||||||
_btnStart.addActionListener(new ActionListener() {
|
_btnStart.addActionListener(new ActionListener() {
|
||||||
@Override public void actionPerformed(ActionEvent arg0) {
|
@Override public void actionPerformed(ActionEvent arg0) {
|
||||||
_btnStart.removeActionListener(this);
|
// ensure no other actions (except for cancel) can be taken while the import is in progress
|
||||||
_btnStart.setEnabled(false);
|
_btnStart.setEnabled(false);
|
||||||
_btnChooseDir.setEnabled(false);
|
_btnChooseDir.setEnabled(false);
|
||||||
|
|
||||||
_disableAll();
|
for (Pair<FCheckBox, ? extends Map<File, File>> selection : _selections.values()) {
|
||||||
|
selection.getLeft().setEnabled(false);
|
||||||
|
}
|
||||||
|
_unknownDeckCombo.setEnabled(false);
|
||||||
|
_moveCheckbox.setEnabled(false);
|
||||||
|
_overwriteCheckbox.setEnabled(false);
|
||||||
|
|
||||||
|
// stop updating the operation log -- the importer needs it now
|
||||||
_operationLogUpdater.requestStop();
|
_operationLogUpdater.requestStop();
|
||||||
|
|
||||||
|
// start importing!
|
||||||
_Importer importer = new _Importer(
|
_Importer importer = new _Importer(
|
||||||
_selections, _unknownDeckCombo, _operationLog, _progressBar,
|
_selections, _unknownDeckCombo, _operationLog, _progressBar,
|
||||||
_moveCheckbox.isSelected(), _overwriteCheckbox.isSelected());
|
_moveCheckbox.isSelected(), _overwriteCheckbox.isSelected());
|
||||||
importer.execute();
|
importer.execute();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// import ready to proceed: enable the start button
|
||||||
_btnStart.setEnabled(true);
|
_btnStart.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// report to the Choose Directory button that this analysis run has stopped
|
||||||
_onAnalyzerDone.run();
|
_onAnalyzerDone.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// asynchronously iterates through the given concurrent maps and populates the operation log with
|
||||||
|
// the proposed operations
|
||||||
private class _OperationLogAsyncUpdater extends Thread {
|
private class _OperationLogAsyncUpdater extends Thread {
|
||||||
final Map<OpType, Map<File, File>> _selections;
|
final Map<OpType, Map<File, File>> _selections;
|
||||||
final JTextArea _operationLog; // safe to set text from another thread
|
final JTextArea _operationLog; // safe to set text from another thread
|
||||||
@@ -532,12 +581,14 @@ public class DialogMigrateProfile {
|
|||||||
_selections = new HashMap<OpType, Map<File, File>>();
|
_selections = new HashMap<OpType, Map<File, File>>();
|
||||||
_operationLog = operationLog;
|
_operationLog = operationLog;
|
||||||
|
|
||||||
// remove references to FCheckBox when adding map -- we can't access it from the other thread anyway
|
// remove references to FCheckBox when populating map -- we can't safely access it from a thread
|
||||||
|
// anyway and it's better to keep our data structure clean to prevent mistakes
|
||||||
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : selections.entrySet()) {
|
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : selections.entrySet()) {
|
||||||
_selections.put(entry.getKey(), entry.getValue().getRight());
|
_selections.put(entry.getKey(), entry.getValue().getRight());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updates the synchronized data with values for the next iteration in _run
|
||||||
public synchronized void requestUpdate(
|
public synchronized void requestUpdate(
|
||||||
Set<OpType> selectedOptions, _UnknownDeckChoice unknownDeckChoice, boolean isMove, boolean isOverwrite) {
|
Set<OpType> selectedOptions, _UnknownDeckChoice unknownDeckChoice, boolean isMove, boolean isOverwrite) {
|
||||||
++_updateCallCnt;
|
++_updateCallCnt;
|
||||||
@@ -572,6 +623,7 @@ public class DialogMigrateProfile {
|
|||||||
if (_stop) { break; }
|
if (_stop) { break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// safely copy synchronized data to local values that we will use for this runthrough
|
||||||
lastUpdateCallCnt = _updateCallCnt;
|
lastUpdateCallCnt = _updateCallCnt;
|
||||||
selectedOptions = _selectedOptions;
|
selectedOptions = _selectedOptions;
|
||||||
unknownDeckChoice = _unknownDeckChoice;
|
unknownDeckChoice = _unknownDeckChoice;
|
||||||
@@ -579,7 +631,7 @@ public class DialogMigrateProfile {
|
|||||||
isOverwrite = _isOverwrite;
|
isOverwrite = _isOverwrite;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set operation summary
|
// build operation log
|
||||||
StringBuilder log = new StringBuilder();
|
StringBuilder log = new StringBuilder();
|
||||||
int totalOps = 0;
|
int totalOps = 0;
|
||||||
for (OpType opType : selectedOptions) {
|
for (OpType opType : selectedOptions) {
|
||||||
@@ -596,6 +648,7 @@ public class DialogMigrateProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// append summary
|
||||||
if (0 < totalOps) {
|
if (0 < totalOps) {
|
||||||
log.append("\n");
|
log.append("\n");
|
||||||
}
|
}
|
||||||
@@ -604,6 +657,7 @@ public class DialogMigrateProfile {
|
|||||||
log.append(isOverwrite ? "O" : "Not o");
|
log.append(isOverwrite ? "O" : "Not o");
|
||||||
log.append("verwriting existing files");
|
log.append("verwriting existing files");
|
||||||
|
|
||||||
|
// set the JTextArea text directly (no need to use invokeLater: setText is thread-safe)
|
||||||
_operationLog.setText(log.toString());
|
_operationLog.setText(log.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -611,10 +665,10 @@ public class DialogMigrateProfile {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try { _run(); } catch (final InterruptedException e) {
|
try { _run(); } catch (final InterruptedException e) {
|
||||||
// we never interrupt the thread, so this is not expected to happen
|
|
||||||
_cancel = true;
|
_cancel = true;
|
||||||
SwingUtilities.invokeLater(new Runnable() {
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
|
// we never interrupt the thread, so this is not expected to happen
|
||||||
BugReporter.reportException(e);
|
BugReporter.reportException(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -622,6 +676,7 @@ public class DialogMigrateProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// asynchronously completes the specified I/O operations and updates the progress bar and operation log
|
||||||
private class _Importer extends SwingWorker<Void, Void> {
|
private class _Importer extends SwingWorker<Void, Void> {
|
||||||
private final Map<File, File> _operations;
|
private final Map<File, File> _operations;
|
||||||
private final JTextArea _operationLog;
|
private final JTextArea _operationLog;
|
||||||
@@ -636,19 +691,16 @@ public class DialogMigrateProfile {
|
|||||||
_move = move;
|
_move = move;
|
||||||
_overwrite = overwrite;
|
_overwrite = overwrite;
|
||||||
|
|
||||||
int totalOps = 0;
|
// build local operations map that only includes data that we can access from the background thread
|
||||||
for (Pair<FCheckBox, ? extends Map<File, File>> selection : selections.values()) {
|
// use a tree map to maintain alphabetical order
|
||||||
if (selection.getLeft().isSelected()) {
|
_operations = new TreeMap<File, File>();
|
||||||
totalOps += selection.getRight().size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_operations = new HashMap<File, File>(totalOps);
|
|
||||||
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : selections.entrySet()) {
|
for (Map.Entry<OpType, Pair<FCheckBox, ? extends Map<File, File>>> entry : selections.entrySet()) {
|
||||||
Pair<FCheckBox, ? extends Map<File, File>> selection = entry.getValue();
|
Pair<FCheckBox, ? extends Map<File, File>> selection = entry.getValue();
|
||||||
if (selection.getLeft().isSelected()) {
|
if (selection.getLeft().isSelected()) {
|
||||||
if (OpType.UNKNOWN_DECK != entry.getKey()) {
|
if (OpType.UNKNOWN_DECK != entry.getKey()) {
|
||||||
_operations.putAll(selection.getRight());
|
_operations.putAll(selection.getRight());
|
||||||
} else {
|
} else {
|
||||||
|
// map unknown decks to selected directory
|
||||||
for (Map.Entry<File, File> op : selection.getRight().entrySet()) {
|
for (Map.Entry<File, File> op : selection.getRight().entrySet()) {
|
||||||
_UnknownDeckChoice choice = (_UnknownDeckChoice)unknownDeckCombo.getSelectedItem();
|
_UnknownDeckChoice choice = (_UnknownDeckChoice)unknownDeckCombo.getSelectedItem();
|
||||||
_operations.put(op.getKey(), new File(choice.path, op.getValue().getName()));
|
_operations.put(op.getKey(), new File(choice.path, op.getValue().getName()));
|
||||||
@@ -656,11 +708,6 @@ public class DialogMigrateProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Pair<FCheckBox, ? extends Map<File, File>> selection : selections.values()) {
|
|
||||||
if (selection.getLeft().isSelected()) {
|
|
||||||
_operations.putAll(selection.getRight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set progress bar bounds
|
// set progress bar bounds
|
||||||
_progressBar.setString(_move ? "Moving files..." : "Copying files...");
|
_progressBar.setString(_move ? "Moving files..." : "Copying files...");
|
||||||
@@ -674,9 +721,9 @@ public class DialogMigrateProfile {
|
|||||||
// working with textbox text is thread safe
|
// working with textbox text is thread safe
|
||||||
_operationLog.setText("");
|
_operationLog.setText("");
|
||||||
|
|
||||||
int numOps = 0;
|
int numOps = 0;
|
||||||
int numSucceeded = 0;
|
int numSucceeded = 0;
|
||||||
int numFailed = 0;
|
int numFailed = 0;
|
||||||
for (Map.Entry<File, File> op : _operations.entrySet()) {
|
for (Map.Entry<File, File> op : _operations.entrySet()) {
|
||||||
if (_cancel) { break; }
|
if (_cancel) { break; }
|
||||||
|
|
||||||
@@ -714,11 +761,14 @@ public class DialogMigrateProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// append summary footer
|
||||||
_operationLog.append(String.format("\nImport complete. %d files %s, %d errors",
|
_operationLog.append(String.format("\nImport complete. %d files %s, %d errors",
|
||||||
numSucceeded, _move ? "moved" : "copied", numFailed));
|
numSucceeded, _move ? "moved" : "copied", numFailed));
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
_cancel = true;
|
_cancel = true;
|
||||||
|
|
||||||
|
// report any exceptions in a standard dialog
|
||||||
|
// note that regular I/O errors don't throw, they'll just be mentioned in the log
|
||||||
SwingUtilities.invokeLater(new Runnable() {
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
_progressBar.setString("Error");
|
_progressBar.setString("Error");
|
||||||
@@ -739,6 +789,7 @@ public class DialogMigrateProfile {
|
|||||||
_onImportSuccessful.run();
|
_onImportSuccessful.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// actual file copy routine. uses java.nio classes for ultra-fast copying
|
||||||
private void _copyFile(File srcFile, File destFile) throws IOException {
|
private void _copyFile(File srcFile, File destFile) throws IOException {
|
||||||
destFile.getParentFile().mkdirs();
|
destFile.getParentFile().mkdirs();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user