+ Drop-To-Unlock Dialog for when the user has to grant access to folders manually via FileDialog or DnD
This commit is contained in:
parent
773f34d9ea
commit
bf5d7141e0
|
@ -0,0 +1,222 @@
|
||||||
|
package net.filebot.mac;
|
||||||
|
|
||||||
|
import static javax.swing.BorderFactory.*;
|
||||||
|
import static net.filebot.UserFiles.*;
|
||||||
|
import static net.filebot.mac.MacAppUtilities.*;
|
||||||
|
import static net.filebot.ui.NotificationLogging.*;
|
||||||
|
import static net.filebot.util.FileUtilities.*;
|
||||||
|
import static net.filebot.util.ui.SwingUI.*;
|
||||||
|
|
||||||
|
import java.awt.BasicStroke;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.Desktop;
|
||||||
|
import java.awt.Dialog.ModalExclusionType;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.Window;
|
||||||
|
import java.awt.datatransfer.Transferable;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.geom.RoundRectangle2D;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JDialog;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
|
||||||
|
import net.filebot.ResourceManager;
|
||||||
|
import net.filebot.ui.HeaderPanel;
|
||||||
|
import net.filebot.ui.transfer.DefaultTransferHandler;
|
||||||
|
import net.filebot.ui.transfer.FileTransferable;
|
||||||
|
import net.filebot.ui.transfer.TransferablePolicy;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
public class DropToUnlock extends JList<File> {
|
||||||
|
|
||||||
|
public static boolean showUnlockDialog(Window owner, Collection<File> folders) {
|
||||||
|
final List<File> model = folders.stream().map(f -> new File(f.getAbsolutePath())).filter(f -> f.isDirectory()).sorted().distinct().collect(Collectors.toList());
|
||||||
|
|
||||||
|
// TODO store secure bookmarks and auto-unlock folders if possible
|
||||||
|
|
||||||
|
// check if we even need to unlock anything
|
||||||
|
if (model.stream().allMatch(f -> !isFolderLocked(f)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
final JDialog dialog = new JDialog(owner);
|
||||||
|
final AtomicBoolean dialogCancelled = new AtomicBoolean(true);
|
||||||
|
DropToUnlock d = new DropToUnlock(model) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateLockStatus(File... folders) {
|
||||||
|
super.updateLockStatus(folders);
|
||||||
|
|
||||||
|
// UI feedback for unlocked folders
|
||||||
|
for (File it : folders) {
|
||||||
|
if (!isFolderLocked(it)) {
|
||||||
|
UILogger.log(Level.INFO, "Folder " + it.getName() + " has been unlocked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all folders have been unlocked auto-close dialog
|
||||||
|
if (model.stream().allMatch(f -> !isFolderLocked(f))) {
|
||||||
|
dialogCancelled.set(false);
|
||||||
|
dialog.setVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
d.setBorder(createEmptyBorder(5, 15, 120, 15));
|
||||||
|
|
||||||
|
JComponent c = (JComponent) dialog.getContentPane();
|
||||||
|
c.setLayout(new MigLayout("insets 0, fill"));
|
||||||
|
|
||||||
|
HeaderPanel h = new HeaderPanel();
|
||||||
|
h.getTitleLabel().setText("Folder Permissions Required");
|
||||||
|
h.getTitleLabel().setIcon(ResourceManager.getIcon("file.lock"));
|
||||||
|
h.getTitleLabel().setBorder(createEmptyBorder(0, 0, 0, 64));
|
||||||
|
c.add(h, "wmin 150px, hmin 75px, growx, dock north");
|
||||||
|
c.add(d, "wmin 150px, hmin 150px, grow");
|
||||||
|
|
||||||
|
dialog.setModal(true);
|
||||||
|
dialog.setModalExclusionType(ModalExclusionType.TOOLKIT_EXCLUDE);
|
||||||
|
dialog.setSize(new Dimension(540, 420));
|
||||||
|
dialog.setResizable(false);
|
||||||
|
dialog.setLocationByPlatform(true);
|
||||||
|
dialog.setAlwaysOnTop(true);
|
||||||
|
|
||||||
|
// open required folders for easy drag and drop
|
||||||
|
invokeLater(750, new Runnable() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
model.stream().map(f -> f.getParentFile()).distinct().forEach(f -> {
|
||||||
|
try {
|
||||||
|
Desktop.getDesktop().open(f);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.getLogger(DropToUnlock.class.getName()).log(Level.WARNING, e.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// show and wait
|
||||||
|
dialog.setVisible(true);
|
||||||
|
|
||||||
|
return !dialogCancelled.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DropToUnlock(Collection<File> model) {
|
||||||
|
super(model.toArray(new File[0]));
|
||||||
|
|
||||||
|
setLayoutOrientation(JList.HORIZONTAL_WRAP);
|
||||||
|
setVisibleRowCount(-1);
|
||||||
|
|
||||||
|
setCellRenderer(new FolderLockCellRenderer());
|
||||||
|
|
||||||
|
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
addMouseListener(new FileChooserAction());
|
||||||
|
|
||||||
|
setTransferHandler(new DefaultTransferHandler(new FolderDropPolicy(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLockStatus(File... folder) {
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final RoundRectangle2D dropArea = new RoundRectangle2D.Double(0, 0, 0, 0, 20, 20);
|
||||||
|
private final BasicStroke dashedStroke = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f, new float[] { 5.0f }, 0.0f);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
super.paintComponent(g);
|
||||||
|
|
||||||
|
Graphics2D g2d = (Graphics2D) g;
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
// draw dashed bounding box
|
||||||
|
int w = 300;
|
||||||
|
int h = 70;
|
||||||
|
int pad = 20;
|
||||||
|
|
||||||
|
g2d.setColor(Color.lightGray);
|
||||||
|
dropArea.setFrameFromCenter(getWidth() / 2, getHeight() - (h / 2) - pad - 10, (getWidth() - w) / 2, getHeight() - h - 2 * pad);
|
||||||
|
g2d.setStroke(dashedStroke);
|
||||||
|
g2d.draw(dropArea);
|
||||||
|
|
||||||
|
// draw text
|
||||||
|
g2d.setColor(Color.gray);
|
||||||
|
g2d.setFont(g2d.getFont().deriveFont(Font.ITALIC, 36));
|
||||||
|
g2d.drawString("Drop 'em", (int) dropArea.getMinX() + 15, (int) dropArea.getMinY() + 40);
|
||||||
|
g2d.drawString("to Unlock 'em", (int) dropArea.getMinX() + 45, (int) dropArea.getMinY() + 40 + 35);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class FolderDropPolicy extends TransferablePolicy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean accept(Transferable tr) throws Exception {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleTransferable(Transferable tr, TransferAction action) throws Exception {
|
||||||
|
List<File> files = FileTransferable.getFilesFromTransferable(tr);
|
||||||
|
if (files != null) {
|
||||||
|
List<File> folders = filter(files, FOLDERS);
|
||||||
|
if (folders.size() > 0) {
|
||||||
|
updateLockStatus(folders.toArray(new File[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class FolderLockCellRenderer extends DefaultListCellRenderer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
return new Dimension(100, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||||
|
File folder = (File) value;
|
||||||
|
JLabel c = (JLabel) super.getListCellRendererComponent(list, folder.getName(), index, false, false);
|
||||||
|
|
||||||
|
c.setIcon(ResourceManager.getIcon(isFolderLocked(folder) ? "folder.locked" : "folder.open"));
|
||||||
|
c.setHorizontalTextPosition(JLabel.CENTER);
|
||||||
|
c.setVerticalTextPosition(JLabel.BOTTOM);
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class FileChooserAction extends MouseAdapter {
|
||||||
|
|
||||||
|
public void mouseClicked(MouseEvent evt) {
|
||||||
|
DropToUnlock list = (DropToUnlock) evt.getSource();
|
||||||
|
if (evt.getClickCount() > 0) {
|
||||||
|
int index = list.locationToIndex(evt.getPoint());
|
||||||
|
if (index >= 0 && list.getCellBounds(index, index).contains(evt.getPoint())) {
|
||||||
|
File folder = list.getModel().getElementAt(index);
|
||||||
|
if (isFolderLocked(folder)) {
|
||||||
|
if (null != showOpenDialogSelectFolder(folder, "Grant Permission", getWindow(list))) {
|
||||||
|
list.updateLockStatus(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package net.filebot.mac;
|
package net.filebot.mac;
|
||||||
|
|
||||||
import java.awt.Window;
|
import java.awt.Window;
|
||||||
|
import java.io.File;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
@ -50,4 +51,8 @@ public class MacAppUtilities {
|
||||||
Logger.getLogger(MacAppUtilities.class.getName()).log(Level.WARNING, "requestForeground not supported: " + t);
|
Logger.getLogger(MacAppUtilities.class.getName()).log(Level.WARNING, "requestForeground not supported: " + t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isFolderLocked(File folder) {
|
||||||
|
return folder.isDirectory() && !folder.canRead() && !folder.canWrite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -989,6 +989,19 @@ public class MediaDetection {
|
||||||
return releaseInfo.getStructureRootPattern().matcher(folder.getName()).matches();
|
return releaseInfo.getStructureRootPattern().matcher(folder.getName()).matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static File getStructureRoot(File file) throws IOException {
|
||||||
|
boolean structureRoot = false;
|
||||||
|
for (File it : listPathTail(file, Integer.MAX_VALUE, true)) {
|
||||||
|
if (structureRoot || isStructureRoot(it)) {
|
||||||
|
if (it.isDirectory()) {
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
structureRoot = true; // find first existing folder at or after the structure root folder (which may not exist yet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static File getStructurePathTail(File file) throws IOException {
|
public static File getStructurePathTail(File file) throws IOException {
|
||||||
LinkedList<String> relativePath = new LinkedList<String>();
|
LinkedList<String> relativePath = new LinkedList<String>();
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
|
@ -17,7 +17,7 @@ import net.filebot.util.ui.GradientStyle;
|
||||||
import net.filebot.util.ui.notification.SeparatorBorder;
|
import net.filebot.util.ui.notification.SeparatorBorder;
|
||||||
import net.filebot.util.ui.notification.SeparatorBorder.Position;
|
import net.filebot.util.ui.notification.SeparatorBorder.Position;
|
||||||
|
|
||||||
class HeaderPanel extends JComponent {
|
public class HeaderPanel extends JComponent {
|
||||||
|
|
||||||
private JLabel titleLabel = new JLabel();
|
private JLabel titleLabel = new JLabel();
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.awt.Window;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.beans.PropertyChangeEvent;
|
import java.beans.PropertyChangeEvent;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.AbstractList;
|
import java.util.AbstractList;
|
||||||
import java.util.AbstractMap.SimpleEntry;
|
import java.util.AbstractMap.SimpleEntry;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -24,6 +25,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
|
@ -44,6 +46,7 @@ import net.filebot.HistorySpooler;
|
||||||
import net.filebot.NativeRenameAction;
|
import net.filebot.NativeRenameAction;
|
||||||
import net.filebot.ResourceManager;
|
import net.filebot.ResourceManager;
|
||||||
import net.filebot.StandardRenameAction;
|
import net.filebot.StandardRenameAction;
|
||||||
|
import net.filebot.mac.DropToUnlock;
|
||||||
import net.filebot.media.MediaDetection;
|
import net.filebot.media.MediaDetection;
|
||||||
import net.filebot.similarity.Match;
|
import net.filebot.similarity.Match;
|
||||||
import net.filebot.util.ui.ProgressDialog;
|
import net.filebot.util.ui.ProgressDialog;
|
||||||
|
@ -148,7 +151,22 @@ class RenameAction extends AbstractAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<File, File> checkRenamePlan(List<Entry<File, File>> renamePlan, Window parent) {
|
private Map<File, File> checkRenamePlan(List<Entry<File, File>> renamePlan, Window parent) throws IOException {
|
||||||
|
// ask for user permissions to output paths
|
||||||
|
if (isMacSandbox()) {
|
||||||
|
Set<File> folders = new TreeSet<File>();
|
||||||
|
for (Entry<File, File> it : renamePlan) {
|
||||||
|
File structureRoot = MediaDetection.getStructureRoot(it.getValue());
|
||||||
|
if (structureRoot != null) {
|
||||||
|
folders.add(structureRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DropToUnlock.showUnlockDialog(parent, folders)) {
|
||||||
|
return emptyMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// build rename map and perform some sanity checks
|
// build rename map and perform some sanity checks
|
||||||
Map<File, File> renameMap = new HashMap<File, File>();
|
Map<File, File> renameMap = new HashMap<File, File>();
|
||||||
Set<File> destinationSet = new HashSet<File>();
|
Set<File> destinationSet = new HashSet<File>();
|
||||||
|
|
Loading…
Reference in New Issue