conversation: support files drag and drop
Change-Id: If97d5b8602ab162b605b1df02c9528d6609d4f0c
diff --git a/src/ConversationVC.h b/src/ConversationVC.h
index d0bce13..7f5bab6 100644
--- a/src/ConversationVC.h
+++ b/src/ConversationVC.h
@@ -43,4 +43,6 @@
-(NSViewController*) getMessagesView;
+-(void)callFinished;
+
@end
diff --git a/src/ConversationVC.mm b/src/ConversationVC.mm
index 6b01470..dd3a2c3 100644
--- a/src/ConversationVC.mm
+++ b/src/ConversationVC.mm
@@ -93,6 +93,10 @@
return messagesViewVC;
}
+-(void)callFinished {
+ [messagesViewVC callFinished];
+}
+
-(void) clearData {
cachedConv_ = nil;
convUid_ = "";
diff --git a/src/MessagesVC.h b/src/MessagesVC.h
index 94fdbc9..e92be95 100644
--- a/src/MessagesVC.h
+++ b/src/MessagesVC.h
@@ -22,9 +22,16 @@
#import <api/conversation.h>
#import <api/avmodel.h>
#import "RecordFileVC.h"
+#import "views/DraggingDestinationView.h"
+@interface PendingFile: NSObject
+@property (retain) NSString* name;
+@property (retain) NSString* size;
+@property (retain) NSImage* preview;
+@property (retain) NSURL* fileUrl;
+@end
-@interface MessagesVC : NSViewController <RecordingViewDelegate>
+@interface MessagesVC : NSViewController <RecordingViewDelegate, DraggingDestinationDelegate>
-(void)setConversationUid:(const QString&)convUid model:(lrc::api::ConversationModel*)model;
-(void)clearData;
@@ -36,7 +43,21 @@
*/
@property (retain) NSString* message;
+/**
+ * This is a KVO method to bind the pending files collection view visibility
+ */
+@property BOOL hideFilesCollection;
+
+/**
+ * This is a KVO method to bind the enable state of send button
+ */
+@property BOOL enableSendButton;
+
-(void) setAVModel: (lrc::api::AVModel*) avmodel;
-(void) checkIfcomposingMsg;
++ (NSMutableDictionary *) pendingFiles;
+
+-(void)callFinished;
+
@end
diff --git a/src/MessagesVC.mm b/src/MessagesVC.mm
index ce0c926..9ecbd65 100644
--- a/src/MessagesVC.mm
+++ b/src/MessagesVC.mm
@@ -30,6 +30,7 @@
#import "views/IMTableCellView.h"
#import "views/MessageBubbleView.h"
#import "views/NSImage+Extensions.h"
+#import "views/FileToSendCollectionItem.h"
#import "delegates/ImageManipulationDelegate.h"
#import "utils.h"
#import "views/NSColor+RingTheme.h"
@@ -41,10 +42,14 @@
#import "RecordFileVC.h"
+@implementation PendingFile
+@end
-@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource, NSTextViewDelegate> {
+@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource, NSTextViewDelegate, NSCollectionViewDataSource> {
__unsafe_unretained IBOutlet NSTableView* conversationView;
+ __unsafe_unretained IBOutlet DraggingDestinationView* draggingDestinationView;
+ __unsafe_unretained IBOutlet NSCollectionView* pendingFilesCollectionView;
__unsafe_unretained IBOutlet NSView* containerView;
__unsafe_unretained IBOutlet TextViewWithPlaceholder* messageView;
__unsafe_unretained IBOutlet IconButton *sendFileButton;
@@ -52,7 +57,6 @@
__unsafe_unretained IBOutlet IconButton *recordAudioButton;
__unsafe_unretained IBOutlet NSLayoutConstraint* sendPanelHeight;
__unsafe_unretained IBOutlet NSLayoutConstraint* messageHeight;
- __unsafe_unretained IBOutlet NSLayoutConstraint* messagesBottomMargin;
__unsafe_unretained IBOutlet NSLayoutConstraint* textBottomConstraint;
IBOutlet NSPopover *recordMessagePopover;
@@ -71,7 +75,7 @@
QMetaObject::Connection lastDisplayedChanged_;
NSString* previewImage;
NSMutableDictionary *pendingMessagesToSend;
- RecordFileVC * recordingController;
+ RecordFileVC* recordingController;
}
@end
@@ -126,6 +130,25 @@
[[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
[messageView setFont: [NSFont systemFontOfSize: 14 weight: NSFontWeightLight]];
[conversationView setWantsLayer:YES];
+ draggingDestinationView.draggingDestinationDelegate = self;
+}
+
+-(void)callFinished {
+ [self reloadPendingFiles];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [conversationView scrollToEndOfDocument:nil];
+ [messageView.window makeFirstResponder: messageView];
+ });
+}
+
++(NSMutableDictionary*)pendingFiles
+{
+ static NSMutableDictionary* files = nil;
+ static dispatch_once_t oncePredicate;
+ dispatch_once(&oncePredicate, ^{
+ files = [[NSMutableDictionary alloc] init];
+ });
+ return files;
}
- (instancetype)initWithCoder:(NSCoder *)coder
@@ -341,25 +364,10 @@
dispatch_async(dispatch_get_main_queue(), ^{
[messageView.window makeFirstResponder: messageView];
});
- conversationView.alphaValue = 0.0;
- [conversationView reloadData];
- [conversationView scrollToEndOfDocument:nil];
- CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
- fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
- fadeIn.toValue = [NSNumber numberWithFloat:1.0];
- fadeIn.duration = 0.4f;
-
- [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
- conversationView.alphaValue = 1;
auto* conv = [self getCurrentConversation];
if (conv == nil)
return;
- try {
- [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
- } catch (std::out_of_range& e) {
- NSLog(@"contact out of range");
- }
NSString* name = bestNameForConversation(*conv, *convModel_);
NSString *placeholder = [NSString stringWithFormat:@"%@%@", @"Write to ", name];
@@ -371,6 +379,22 @@
nil];
NSAttributedString* attributedPlaceholder = [[NSAttributedString alloc] initWithString: placeholder attributes:nameAttrs];
messageView.placeholderAttributedString = attributedPlaceholder;
+ [self reloadPendingFiles];
+ conversationView.alphaValue = 0.0;
+ [conversationView reloadData];
+ [conversationView scrollToEndOfDocument:nil];
+ CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
+ fadeIn.toValue = [NSNumber numberWithFloat:1.0];
+ fadeIn.duration = 0.4f;
+
+ [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
+ conversationView.alphaValue = 1;
+ try {
+ [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
+ } catch (std::out_of_range& e) {
+ NSLog(@"contact out of range");
+ }
}
#pragma mark - configure cells
@@ -960,7 +984,6 @@
if (MESSAGE_VIEW_DEFAULT_HEIGHT != lineHeight) {
MESSAGE_VIEW_DEFAULT_HEIGHT = lineHeight;
}
- messagesBottomMargin.constant = newSendPanelHeight;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self scrollToBottom];
messageHeight.constant = msgHeight;
@@ -1068,6 +1091,14 @@
unichar separatorChar = NSLineSeparatorCharacter;
NSString *separatorString = [NSString stringWithCharacters:&separatorChar length:1];
text = [text stringByReplacingOccurrencesOfString: separatorString withString: @"\n"];
+ // send files
+ NSMutableArray* files = [MessagesVC pendingFiles][convUid_.toNSString()];
+ for (PendingFile* file : files) {
+ convModel_->sendFile(convUid_, QString::fromNSString(file.fileUrl.path), QString::fromNSString(file.name));
+ }
+ [MessagesVC.pendingFiles removeObjectForKey: convUid_.toNSString()];
+ [self reloadPendingFiles];
+
if (text && text.length > 0) {
auto* conv = [self getCurrentConversation];
if (conv == nil)
@@ -1086,7 +1117,6 @@
if(messageHeight.constant != MESSAGE_VIEW_DEFAULT_HEIGHT) {
sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
messageHeight.constant = MESSAGE_VIEW_DEFAULT_HEIGHT;
- messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
textBottomConstraint.constant = BOTTOM_MARGIN;
[self scrollToBottom];
}
@@ -1143,18 +1173,10 @@
NSOpenPanel* filePicker = [NSOpenPanel openPanel];
[filePicker setCanChooseFiles:YES];
[filePicker setCanChooseDirectories:NO];
- [filePicker setAllowsMultipleSelection:NO];
+ [filePicker setAllowsMultipleSelection:YES];
if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
- if ([[filePicker URLs] count] == 1) {
- NSURL* url = [[filePicker URLs] objectAtIndex:0];
- const char* fullPath = [url fileSystemRepresentation];
- NSString* fileName = [url lastPathComponent];
- if (convModel_) {
- auto* conv = [self getCurrentConversation];
- convModel_->sendFile(convUid_, QString::fromStdString(fullPath), QString::fromNSString(fileName));
- }
- }
+ [self filesDragged: [filePicker URLs]];
}
}
@@ -1163,7 +1185,7 @@
- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
if (commandSelector == @selector(insertNewline:)) {
- if(self.message.length > 0) {
+ if(self.message.length > 0 || [(NSMutableArray*)MessagesVC. pendingFiles[convUid_.toNSString()] count] > 0) {
[self sendMessage: nil];
return YES;
}
@@ -1173,8 +1195,9 @@
return NO;
}
--(void) textDidChange:(NSNotification *)notification {
+-(void)textDidChange:(NSNotification *)notification {
[self checkIfComposingMsg];
+ self.enableSendButton = self.message.length > 0 || [(NSMutableArray*)MessagesVC. pendingFiles[convUid_.toNSString()] count] > 0;
}
- (void) checkIfComposingMsg {
@@ -1226,7 +1249,6 @@
}
}
-
- (void)removeFromResponderChain {
if (conversationView.window &&
[[conversationView.window nextResponder] isEqual:self]) {
@@ -1235,4 +1257,78 @@
}
}
+#pragma mark - NSCollectionViewDataSource
+
+- (NSInteger)collectionView:(NSCollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
+ return [(NSMutableArray*)MessagesVC.pendingFiles[convUid_.toNSString()] count];
+}
+- (NSCollectionViewItem*)collectionView:(NSCollectionView *)collectionView itemForRepresentedObjectAtIndexPath:(NSIndexPath *)indexPath {
+ FileToSendCollectionItem* fileCell = [collectionView makeItemWithIdentifier:@"FileToSendCollectionItem" forIndexPath:indexPath];
+ PendingFile* file = MessagesVC.pendingFiles[convUid_.toNSString()][indexPath.item];
+ fileCell.filePreview.image = file.preview;
+ fileCell.fileName.stringValue = file.name;
+ fileCell.fileName.toolTip = file.name;
+ fileCell.fileSize.stringValue = file.size;
+ [fileCell.closeButton setAction:@selector(removePendingFile:)];
+ fileCell.closeButton.tag = indexPath.item;
+ [fileCell.closeButton setTarget:self];
+ return fileCell;
+}
+
+#pragma mark - DraggingDestinationDelegate
+
+-(void)filesDragged:(NSArray*)urls {
+ [self prepareFilesToSend: urls];
+}
+
+-(NSString*)convertBytedToString:(double)bytes {
+ if (bytes <= 1000) {
+ return [NSString stringWithFormat:@"%.2f%@", bytes, @" B"];
+ } else if (bytes <= 1e6) {
+ return [NSString stringWithFormat:@"%.2f%@",(bytes * 1e-3), @" KB"];
+ } else if (bytes <= 1e9) {
+ return [NSString stringWithFormat:@"%.2f%@",(bytes * 1e-6), @" MB"];
+ }
+ return [NSString stringWithFormat:@"%.2f%@",(bytes * 1e-9), @" GB"];
+}
+
+-(void)prepareFilesToSend:(NSArray*)urls {
+ NSMutableArray* files = [[NSMutableArray alloc] init];
+ NSMutableArray* existingFiles = MessagesVC.pendingFiles[convUid_.toNSString()];
+ [files addObjectsFromArray: existingFiles];
+ for (NSURL* url : urls) {
+ NSString* filePath = [url path];
+ NSImage* preview = [[NSImage alloc] initWithContentsOfFile: filePath];
+ NSString* name = [url lastPathComponent];
+ NSData* documentBytes = [[NSData alloc] initWithContentsOfFile: filePath];
+ PendingFile* file = [[PendingFile alloc] init];
+ file.name = name;
+ file.size = [self convertBytedToString: documentBytes.length];
+ file.preview = preview;
+ file.fileUrl = url;
+ [files addObject: file];
+ }
+ MessagesVC.pendingFiles[convUid_.toNSString()] = files;
+ [self reloadPendingFiles];
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ [self scrollToBottom];
+ });
+}
+
+- (void)removePendingFile:(id)sender {
+ NSButton* closeButton = (NSButton*)sender;
+ NSInteger index = closeButton.tag;
+ if(index < 0) {
+ return;
+ }
+ [MessagesVC.pendingFiles[convUid_.toNSString()] removeObjectAtIndex:index];
+ [self reloadPendingFiles];
+}
+
+- (void)reloadPendingFiles {
+ self.hideFilesCollection = [(NSMutableArray*)MessagesVC .pendingFiles[convUid_.toNSString()] count]== 0;
+ self.enableSendButton = self.message.length > 0 || [(NSMutableArray*)MessagesVC. pendingFiles[convUid_.toNSString()] count] > 0;
+ [pendingFilesCollectionView reloadData];
+}
+
@end
diff --git a/src/RingWindowController.mm b/src/RingWindowController.mm
index 7b1d5de..8998ff0 100644
--- a/src/RingWindowController.mm
+++ b/src/RingWindowController.mm
@@ -649,6 +649,7 @@
-(void) callFinished {
[self changeViewTo:SHOW_CONVERSATION_SCREEN];
+ [conversationVC callFinished];
}
-(void) showConversation:(NSString* )conversationId forAccount:(NSString*)accountId {
diff --git a/src/views/DraggingDestinationView.h b/src/views/DraggingDestinationView.h
new file mode 100644
index 0000000..452991d
--- /dev/null
+++ b/src/views/DraggingDestinationView.h
@@ -0,0 +1,43 @@
+/*
+* Copyright (C) 2021 Savoir-faire Linux Inc.
+* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#import <Cocoa/Cocoa.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol DraggingDestinationDelegate
+-(void) filesDragged:(NSArray*)urls;
+@end
+
+@interface DraggingDestinationView : NSView <NSDraggingDestination>
+{
+/**
+ * Indicate when dragged file enter drop zone
+ */
+BOOL highlight;
+}
+
+/**
+ * Delegate to inform about files to process
+ */
+@property (nonatomic) id <DraggingDestinationDelegate> draggingDestinationDelegate;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/views/DraggingDestinationView.mm b/src/views/DraggingDestinationView.mm
new file mode 100644
index 0000000..f53c157
--- /dev/null
+++ b/src/views/DraggingDestinationView.mm
@@ -0,0 +1,89 @@
+/*
+* Copyright (C) 2021 Savoir-faire Linux Inc.
+* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#import "DraggingDestinationView.h"
+
+@implementation DraggingDestinationView
+- (id)initWithFrame:(NSRect)frame
+{
+ if (self = [super initWithFrame: frame])
+ [self registerForDraggedTypes:[NSArray arrayWithObjects: NSPasteboardTypeURL, nil]];
+ return self;
+}
+
+-(void)drawRect:(NSRect)rect
+{
+ [super drawRect:rect];
+
+ if (highlight) {
+ [[[NSColor blackColor] colorWithAlphaComponent:0.8] set];
+ [NSBezierPath fillRect: rect];
+ }
+
+ NSRect rectText = rect;
+ NSDictionary* attributes = nil;
+
+ NSString* title = highlight ?
+ NSLocalizedString(@"Drop files to send", @"drop files") : @"";
+ NSMutableParagraphStyle* style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
+ [style setLineBreakMode:NSLineBreakByWordWrapping];
+ [style setAlignment:NSCenterTextAlignment];
+ NSFont* font= [NSFont systemFontOfSize: 32 weight: NSFontWeightSemibold];
+ attributes = [[NSDictionary alloc] initWithObjectsAndKeys:
+ style, NSParagraphStyleAttributeName,
+ font,NSFontAttributeName,
+ [NSColor whiteColor],
+ NSForegroundColorAttributeName, nil];
+ rectText.size = [title sizeWithAttributes: attributes];
+ rectText.origin.x = floor( NSMidX(rect) - rectText.size.width / 2);
+ rectText.origin.y = floor( NSMidY([self bounds]) - rectText.size.height / 2 );
+ [title drawInRect:rectText withAttributes: attributes];
+}
+
+#pragma mark - Destination Operations
+
+- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
+{
+ highlight = true;
+ [self setNeedsDisplay: true];
+ return NSDragOperationCopy;
+}
+
+- (void)draggingExited:(id <NSDraggingInfo>)sender
+{
+ highlight = false;
+ [self setNeedsDisplay: true];
+}
+
+- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
+{
+ highlight = false;
+ [self setNeedsDisplay: true];
+ return true;
+}
+
+- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
+{
+ NSArray* classArray = [NSArray arrayWithObject:[NSURL class]];
+ NSArray* arrayOfURLs = [[sender draggingPasteboard] readObjectsForClasses:classArray options:nil];
+ [self.draggingDestinationDelegate filesDragged: arrayOfURLs];
+ return true;
+}
+
+@end
diff --git a/src/views/FileToSendCollectionItem.h b/src/views/FileToSendCollectionItem.h
new file mode 100644
index 0000000..9eff5ae
--- /dev/null
+++ b/src/views/FileToSendCollectionItem.h
@@ -0,0 +1,34 @@
+/*
+* Copyright (C) 2021 Savoir-faire Linux Inc.
+* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#import <Cocoa/Cocoa.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FileToSendCollectionItem : NSCollectionViewItem
+
+@property (nonatomic, strong) IBOutlet NSImageView* filePreview;
+@property (nonatomic, strong) IBOutlet NSImageView* placeholderPreview;
+@property (nonatomic, strong) IBOutlet NSTextField* fileName;
+@property (nonatomic, strong) IBOutlet NSTextField* fileSize;
+@property (nonatomic, strong) IBOutlet NSButton* closeButton;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/views/FileToSendCollectionItem.mm b/src/views/FileToSendCollectionItem.mm
new file mode 100644
index 0000000..7bd3bb4
--- /dev/null
+++ b/src/views/FileToSendCollectionItem.mm
@@ -0,0 +1,47 @@
+/*
+* Copyright (C) 2021 Savoir-faire Linux Inc.
+* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#import "FileToSendCollectionItem.h"
+#import "NSColor+RingTheme.h"
+
+@interface FileToSendCollectionItem ()
+
+@end
+
+@implementation FileToSendCollectionItem
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ self.placeholderPreview.image = [NSColor image: [NSImage imageNamed:@"ic_file.png"] tintedWithColor: [NSColor windowFrameTextColor]];
+ self.closeButton.image = [NSColor image: [NSImage imageNamed:@"ic_action_cancel.png"] tintedWithColor: [NSColor windowFrameTextColor]];
+ [NSDistributedNotificationCenter.defaultCenter addObserver:self selector:@selector(themeChanged:) name:@"AppleInterfaceThemeChangedNotification" object: nil];
+}
+
+-(void) deinit {
+ [NSDistributedNotificationCenter.defaultCenter removeObserver:self];
+}
+
+-(void)themeChanged:(NSNotification *) notification {
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ self.placeholderPreview.image = [NSColor image: [NSImage imageNamed:@"ic_file.png"] tintedWithColor: [NSColor windowFrameTextColor]];
+ self.closeButton.image = [NSColor image: [NSImage imageNamed:@"ic_action_cancel.png"] tintedWithColor: [NSColor windowFrameTextColor]];
+ });
+}
+
+@end