blob: 6bf4f59bffd1bec1aea0b3f9154d48df5731c7af [file] [log] [blame]
/*
* Copyright (C) 2015-2016 Savoir-faire Linux Inc.
* Author: Alexandre Lision <alexandre.lision@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 "CurrentCallVC.h"
extern "C" {
#import "libavutil/frame.h"
#import "libavutil/display.h"
}
#import <QuartzCore/QuartzCore.h>
///Qt
#import <QMimeData>
#import <QtMacExtras/qmacfunctions.h>
#import <QtCore/qabstractitemmodel.h>
#import <QPixmap>
#import <QUrl>
///LRC
#import <video/renderer.h>
#import <api/newcallmodel.h>
#import <api/call.h>
#import <api/conversationmodel.h>
#import <api/avmodel.h>
#import <globalinstances.h>
#import "AppDelegate.h"
#import "views/ITProgressIndicator.h"
#import "views/CallView.h"
#import "views/NSColor+RingTheme.h"
#import "delegates/ImageManipulationDelegate.h"
#import "ChatVC.h"
#import "views/IconButton.h"
#import "utils.h"
#import "views/CallMTKView.h"
#import "VideoCommon.h"
#import "views/GradientView.h"
#import "views/MovableView.h"
@interface RendererConnectionsHolder : NSObject
@property QMetaObject::Connection frameUpdated;
@property QMetaObject::Connection started;
@property QMetaObject::Connection stopped;
@end
@implementation RendererConnectionsHolder
@end
@interface CurrentCallVC () <NSPopoverDelegate> {
std::string convUid_;
std::string callUid_;
const lrc::api::account::Info *accountInfo_;
NSTimer* refreshDurationTimer;
}
// Main container
@property (unsafe_unretained) IBOutlet NSSplitView* splitView;
@property (unsafe_unretained) IBOutlet NSView* backgroundImage;
@property (unsafe_unretained) IBOutlet NSBox* bluerBackgroundEffect;
// Header info
@property (unsafe_unretained) IBOutlet NSView* headerContainer;
@property (unsafe_unretained) IBOutlet NSTextField* timeSpentLabel;
// info
@property (unsafe_unretained) IBOutlet NSStackView* infoContainer;
@property (unsafe_unretained) IBOutlet NSImageView* contactPhoto;
@property (unsafe_unretained) IBOutlet NSTextField* contactNameLabel;
@property (unsafe_unretained) IBOutlet NSTextField* callStateLabel;
@property (unsafe_unretained) IBOutlet NSTextField* contactIdLabel;
@property (unsafe_unretained) IBOutlet IconButton* cancelCallButton;
@property (unsafe_unretained) IBOutlet IconButton* pickUpButton;
@property (unsafe_unretained) IBOutlet ITProgressIndicator *loadingIndicator;
// Call Controls
@property (unsafe_unretained) IBOutlet GradientView* controlsPanel;
@property (unsafe_unretained) IBOutlet IconButton* holdOnOffButton;
@property (unsafe_unretained) IBOutlet IconButton* hangUpButton;
@property (unsafe_unretained) IBOutlet IconButton* recordOnOffButton;
@property (unsafe_unretained) IBOutlet IconButton* muteAudioButton;
@property (unsafe_unretained) IBOutlet IconButton* muteVideoButton;
@property (unsafe_unretained) IBOutlet IconButton* transferButton;
@property (unsafe_unretained) IBOutlet IconButton* addParticipantButton;
@property (unsafe_unretained) IBOutlet IconButton* chatButton;
@property (unsafe_unretained) IBOutlet IconButton* qualityButton;
// Video
@property (unsafe_unretained) IBOutlet CallView *videoView;
@property (unsafe_unretained) IBOutlet CallMTKView *previewView;
@property (unsafe_unretained) IBOutlet MovableView *movableBaseForView;
@property (unsafe_unretained) IBOutlet NSView* hidePreviewBackground;
@property (unsafe_unretained) IBOutlet NSButton* hidePreviewButton;
@property (unsafe_unretained) IBOutlet CallMTKView *videoMTKView;
@property RendererConnectionsHolder* renderConnections;
@property QMetaObject::Connection videoStarted;
@property QMetaObject::Connection selectedCallChanged;
@property QMetaObject::Connection messageConnection;
@property QMetaObject::Connection mediaAddedConnection;
@property QMetaObject::Connection profileUpdatedConnection;
@property (strong) IBOutlet ChatVC* chatVC;
@end
@implementation CurrentCallVC
lrc::api::AVModel* mediaModel;
NSInteger const PREVIEW_WIDTH = 185;
NSInteger const PREVIEW_HEIGHT = 130;
NSInteger const HIDE_PREVIEW_BUTTON_MIN_SIZE = 25;
NSInteger const HIDE_PREVIEW_BUTTON_MAX_SIZE = 35;
NSInteger const PREVIEW_MARGIN = 20;
@synthesize holdOnOffButton, hangUpButton, recordOnOffButton, pickUpButton, chatButton, transferButton, addParticipantButton, timeSpentLabel, muteVideoButton, muteAudioButton, controlsPanel, headerContainer, videoView, previewView, splitView, loadingIndicator, backgroundImage, bluerBackgroundEffect, hidePreviewButton, hidePreviewBackground, movableBaseForView, infoContainer, contactPhoto, contactNameLabel, callStateLabel, contactIdLabel, cancelCallButton;
@synthesize renderConnections;
CVPixelBufferPoolRef pixelBufferPoolDistantView;
CVPixelBufferRef pixelBufferDistantView;
CVPixelBufferPoolRef pixelBufferPoolPreview;
CVPixelBufferRef pixelBufferPreview;
-(void) setCurrentCall:(const std::string&)callUid
conversation:(const std::string&)convUid
account:(const lrc::api::account::Info*)account
avModel:(lrc::api::AVModel *)avModel;
{
if(account == nil)
return;
mediaModel = avModel;
auto* callModel = account->callModel.get();
if (not callModel->hasCall(callUid)){
callUid_.clear();
return;
}
callUid_ = callUid;
convUid_ = convUid;
accountInfo_ = account;
[self.chatVC setConversationUid:convUid model:account->conversationModel.get()];
auto currentCall = callModel->getCall(callUid_);
[self setUpButtons: currentCall isRecording: (callModel->isRecording(callUid_) || avModel->getAlwaysRecord())];
[previewView setHidden: YES];
videoView.callId = callUid;
[self setUpPrewviewFrame];
}
-(void) setUpButtons:(lrc::api::call::Info&)callInfo isRecording:(BOOL) isRecording {
muteAudioButton.image = callInfo.audioMuted ? [NSImage imageNamed:@"ic_action_mute_audio.png"] :
[NSImage imageNamed:@"ic_action_audio.png"];
muteVideoButton.image = callInfo.videoMuted ? [NSImage imageNamed:@"ic_action_mute_video.png"] :
[NSImage imageNamed:@"ic_action_video.png"];
[muteVideoButton setHidden: callInfo.isAudioOnly ? YES: NO];
if (isRecording) {
[recordOnOffButton startBlinkAnimationfrom:[NSColor buttonBlinkColorColor] to:[NSColor whiteColor] scaleFactor: 1 duration: 1.5];
} else {
[recordOnOffButton stopBlinkAnimation];
}
}
- (void) setUpPrewviewFrame {
CGPoint previewOrigin = CGPointMake(self.videoView.frame.size.width - PREVIEW_WIDTH - PREVIEW_MARGIN, PREVIEW_MARGIN);
movableBaseForView.frame = CGRectMake(previewOrigin.x, previewOrigin.y, PREVIEW_WIDTH, PREVIEW_HEIGHT);
self.movableBaseForView.movable = true;
previewView.frame = movableBaseForView.bounds;
hidePreviewBackground.frame = [self frameForExpendPreviewButton: false];;
}
- (void)awakeFromNib
{
[self.view setWantsLayer:YES];
renderConnections = [[RendererConnectionsHolder alloc] init];
[loadingIndicator setColor:[NSColor whiteColor]];
[loadingIndicator setNumberOfLines:200];
[loadingIndicator setWidthOfLine:4];
[loadingIndicator setLengthOfLine:4];
[loadingIndicator setInnerMargin:59];
[self.videoView setCallDelegate:self];
CGColor* color = [[[NSColor blackColor] colorWithAlphaComponent:0.2] CGColor];
[headerContainer.layer setBackgroundColor:color];
[bluerBackgroundEffect setWantsLayer:YES];
bluerBackgroundEffect.alphaValue = 0.6;
[backgroundImage setWantsLayer: YES];
backgroundImage.layer.contentsGravity = kCAGravityResizeAspectFill;
movableBaseForView.wantsLayer = YES;
movableBaseForView.layer.cornerRadius = 4;
movableBaseForView.layer.masksToBounds = true;
movableBaseForView.hostingView = self.videoView;
[movableBaseForView setAutoresizingMask: NSViewNotSizable | NSViewMaxXMargin | NSViewMaxYMargin | NSViewMinXMargin | NSViewMinYMargin];
[previewView setAutoresizingMask: NSViewNotSizable | NSViewMaxXMargin | NSViewMaxYMargin | NSViewMinXMargin | NSViewMinYMargin];
[hidePreviewBackground setAutoresizingMask: NSViewNotSizable | NSViewMaxXMargin | NSViewMaxYMargin | NSViewMinXMargin | NSViewMinYMargin];
}
-(void) updateDurationLabel
{
if (accountInfo_ != nil) {
auto* callModel = accountInfo_->callModel.get();
if (callModel->hasCall(callUid_)) {
auto& callStatus = callModel->getCall(callUid_).status;
if (callStatus != lrc::api::call::Status::ENDED &&
callStatus != lrc::api::call::Status::TERMINATING &&
callStatus != lrc::api::call::Status::INVALID) {
[timeSpentLabel setStringValue:@(callModel->getFormattedCallDuration(callUid_).c_str())];
return;
}
}
}
// If call is not running anymore or accountInfo_ is not set for any reason
// we stop the refresh loop
[refreshDurationTimer invalidate];
refreshDurationTimer = nil;
}
-(void) updateCall
{
if (accountInfo_ == nil)
return;
auto* callModel = accountInfo_->callModel.get();
if (not callModel->hasCall(callUid_)) {
return;
}
auto currentCall = callModel->getCall(callUid_);
NSLog(@"\n status %@ \n",@(lrc::api::call::to_string(currentCall.status).c_str()));
auto convIt = getConversationFromUid(convUid_, *accountInfo_->conversationModel);
if (convIt != accountInfo_->conversationModel->allFilteredConversations().end()) {
NSString* bestName = bestNameForConversation(*convIt, *accountInfo_->conversationModel);
[contactNameLabel setStringValue:bestName];
NSString* ringID = bestIDForConversation(*convIt, *accountInfo_->conversationModel);
if([bestName isEqualToString:ringID]) {
ringID = @"";
}
[contactIdLabel setStringValue:ringID];
}
[self setupContactInfo:contactPhoto];
[timeSpentLabel setStringValue:@(callModel->getFormattedCallDuration(callUid_).c_str())];
if (refreshDurationTimer == nil)
refreshDurationTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateDurationLabel)
userInfo:nil
repeats:YES];
[self setBackground];
using Status = lrc::api::call::Status;
if (currentCall.status == Status::PAUSED) {
[holdOnOffButton startBlinkAnimationfrom:[NSColor buttonBlinkColorColor] to:[NSColor whiteColor] scaleFactor: 1.0 duration: 1.5];
} else {
[holdOnOffButton stopBlinkAnimation];
}
callStateLabel.stringValue = currentCall.status == Status::INCOMING_RINGING ? @"wants to talk to you" : @(to_string(currentCall.status).c_str());
loadingIndicator.hidden = (currentCall.status == Status::SEARCHING ||
currentCall.status == Status::CONNECTING ||
currentCall.status == Status::OUTGOING_RINGING) ? NO : YES;
pickUpButton.hidden = (currentCall.status == Status::INCOMING_RINGING) ? NO : YES;
callStateLabel.hidden = (currentCall.status == Status::IN_PROGRESS) ? YES : NO;
cancelCallButton.hidden = (currentCall.status == Status::IN_PROGRESS ||
currentCall.status == Status::PAUSED) ? YES : NO;
switch (currentCall.status) {
case Status::SEARCHING:
case Status::CONNECTING:
case Status::OUTGOING_RINGING:
case Status::INCOMING_RINGING:
[infoContainer setHidden: NO];
[headerContainer setHidden:YES];
[controlsPanel setHidden:YES];
break;
/*case Status::CONFERENCE:
[self setupConference:currentCall];
break;*/
case Status::PAUSED:
[infoContainer setHidden: NO];
[headerContainer setHidden:NO];
[controlsPanel setHidden:NO];
[backgroundImage setHidden:NO];
[bluerBackgroundEffect setHidden:NO];
if(!currentCall.isAudioOnly) {
[self.videoMTKView fillWithBlack];
[self.previewView fillWithBlack];
[hidePreviewBackground setHidden:YES];
[self.previewView setHidden: YES];
[self.videoMTKView setHidden: YES];
self.previewView.stopRendering = true;
self.videoMTKView.stopRendering = true;
}
break;
case Status::INACTIVE:
if(currentCall.isAudioOnly) {
[self setUpAudioOnlyView];
} else {
[self setUpVideoCallView];
}
break;
case Status::IN_PROGRESS:
[headerContainer setHidden:NO];
[controlsPanel setHidden:NO];
if(currentCall.isAudioOnly) {
[self setUpAudioOnlyView];
} else {
[self setUpVideoCallView];
}
break;
case Status::CONNECTED:
break;
case Status::ENDED:
case Status::TERMINATING:
case Status::INVALID:
break;
}
}
-(void) setUpVideoCallView {
[previewView fillWithBlack];
[self.videoMTKView fillWithBlack];
[previewView setHidden: NO];
[self.videoMTKView setHidden:NO];
[hidePreviewBackground setHidden: self.previewView.stopRendering];
[bluerBackgroundEffect setHidden:YES];
[backgroundImage setHidden:YES];
self.previewView.stopRendering = false;
self.videoMTKView.stopRendering = false;
}
-(void) setUpAudioOnlyView {
[self.previewView setHidden: YES];
[self.videoMTKView setHidden: YES];
[hidePreviewBackground setHidden: YES];
[bluerBackgroundEffect setHidden:NO];
[backgroundImage setHidden:NO];
}
-(void) setBackground {
auto* convModel = accountInfo_->conversationModel.get();
auto it = getConversationFromUid(convUid_, *convModel);
NSImage *image= [self getContactImageOfSize:120.0 withDefaultAvatar:NO];
if(image) {
CIImage * ciImage = [[CIImage alloc] initWithData:[image TIFFRepresentation]];
CIContext *context = [[CIContext alloc] init];
CIFilter *clamp = [CIFilter filterWithName:@"CIAffineClamp"];
[clamp setValue:[NSAffineTransform transform] forKey:@"inputTransform"];
[clamp setValue:ciImage forKey: kCIInputImageKey];
CIFilter* bluerFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
[bluerFilter setDefaults];
[bluerFilter setValue:[NSNumber numberWithFloat: 9] forKey:@"inputRadius"];
[bluerFilter setValue:[clamp valueForKey:kCIOutputImageKey] forKey: kCIInputImageKey];
CIImage *result = [bluerFilter valueForKey:kCIOutputImageKey];
CGRect extent = [result extent];
CGImageRef cgImage = [context createCGImage:result fromRect: [ciImage extent]];
NSImage *bluredImage = [[NSImage alloc] initWithCGImage:cgImage size:NSSizeFromCGSize(CGSizeMake(image.size.width, image.size.height))];
backgroundImage.layer.contents = bluredImage;
[backgroundImage setHidden:NO];
} else {
contactNameLabel.textColor = [NSColor darkGrayColor];
contactNameLabel.textColor = [NSColor darkGrayColor];
contactIdLabel.textColor = [NSColor darkGrayColor];
callStateLabel.textColor = [NSColor darkGrayColor];
backgroundImage.layer.contents = nil;
[bluerBackgroundEffect setFillColor:[NSColor ringGreyHighlight]];
[bluerBackgroundEffect setAlphaValue:1];
[backgroundImage setHidden:YES];
}
}
-(NSImage *) getContactImageOfSize: (double) size withDefaultAvatar:(BOOL) shouldDrawDefault {
auto* convModel = accountInfo_->conversationModel.get();
auto convIt = getConversationFromUid(convUid_, *convModel);
if (convIt == convModel->allFilteredConversations().end()) {
return nil;
}
if(shouldDrawDefault) {
auto& imgManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
QVariant photo = imgManip.conversationPhoto(*convIt, *accountInfo_, QSize(size, size), NO);
return QtMac::toNSImage(qvariant_cast<QPixmap>(photo));
}
auto contact = accountInfo_->contactModel->getContact(convIt->participants[0]);
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:@(contact.profileInfo.avatar.c_str()) options:NSDataBase64DecodingIgnoreUnknownCharacters];
return [[NSImage alloc] initWithData:imageData];
}
-(void) setupContactInfo:(NSImageView*)imageView
{
[imageView setImage: [self getContactImageOfSize:120.0 withDefaultAvatar:YES]];
}
-(void) setupConference
{
[videoView setShouldAcceptInteractions:YES];
[self.chatButton setHidden:NO];
[self.addParticipantButton setHidden:NO];
[self.transferButton setHidden:YES];
}
-(void)collapseRightView
{
NSView *right = [[splitView subviews] objectAtIndex:1];
NSView *left = [[splitView subviews] objectAtIndex:0];
NSRect leftFrame = [left frame];
[right setHidden:YES];
[splitView display];
}
- (void) changeCallSelection:(std::string&)callUid
{
if (accountInfo_ == nil)
return;
auto* callModel = accountInfo_->callModel.get();
if (not callModel->hasCall(callUid)) {
return;
}
QObject::disconnect(self.selectedCallChanged);
self.selectedCallChanged = QObject::connect(callModel,
&lrc::api::NewCallModel::callStatusChanged,
[self](const std::string callId) {
[self updateCall];
});
}
-(void) connectVideoSignals
{
if (accountInfo_ == nil)
return;
[self connectRenderer];
}
-(void) connectRenderer
{
QObject::disconnect(renderConnections.frameUpdated);
QObject::disconnect(renderConnections.stopped);
QObject::disconnect(renderConnections.started);
renderConnections.started =
QObject::connect(mediaModel,
&lrc::api::AVModel::rendererStarted,
[=](const std::string& id) {
if (id == lrc::api::video::PREVIEW_RENDERER_ID) {
[self.previewView setHidden:NO];
self.previewView.stopRendering = false;
} else {
[self mouseIsMoving: NO];
self.videoMTKView.stopRendering = false;
[self.videoMTKView setHidden:NO];
[bluerBackgroundEffect setHidden:YES];
[backgroundImage setHidden:YES];
[videoView setShouldAcceptInteractions:YES];
}
QObject::disconnect(renderConnections.frameUpdated);
renderConnections.frameUpdated =
QObject::connect(mediaModel,
&lrc::api::AVModel::frameUpdated,
[=](const std::string& id) {
if (id == lrc::api::video::PREVIEW_RENDERER_ID) {
auto renderer = &mediaModel->getRenderer(lrc::api::video::PREVIEW_RENDERER_ID);
if(!renderer->isRendering()) {
return;
}
[hidePreviewBackground setHidden: NO];
[self renderer: renderer renderFrameForPreviewView:previewView];
} else {
auto renderer = &mediaModel->getRenderer(id);
if(!renderer->isRendering()) {
return;
}
[self renderer:renderer renderFrameForDistantView: self.videoMTKView];
}
});
});
renderConnections.stopped =
QObject::connect(mediaModel,
&lrc::api::AVModel::rendererStopped,
[=](const std::string& id) {
QObject::disconnect(renderConnections.frameUpdated);
if (id == lrc::api::video::PREVIEW_RENDERER_ID) {
[self.previewView setHidden:YES];
self.previewView.stopRendering = true;
} else {
[self mouseIsMoving: YES];
self.videoMTKView.stopRendering = true;
[self.videoMTKView setHidden:YES];
[bluerBackgroundEffect setHidden:NO];
[backgroundImage setHidden:NO];
[videoView setShouldAcceptInteractions:NO];
}
});
renderConnections.frameUpdated =
QObject::connect(mediaModel,
&lrc::api::AVModel::frameUpdated,
[=](const std::string& id) {
if (id == lrc::api::video::PREVIEW_RENDERER_ID) {
auto renderer = &mediaModel->getRenderer(lrc::api::video::PREVIEW_RENDERER_ID);
if(!renderer->isRendering()) {
return;
}
[self renderer: renderer renderFrameForPreviewView:previewView];
} else {
auto renderer = &mediaModel->getRenderer(id);
if(!renderer->isRendering()) {
return;
}
[self renderer:renderer renderFrameForDistantView: self.videoMTKView];
}
});
}
-(void) renderer: (const lrc::api::video::Renderer*)renderer renderFrameForPreviewView:(CallMTKView*) view
{
@autoreleasepool {
auto framePtr = renderer->currentAVFrame();
auto frame = framePtr.get();
if(!frame || !frame->width || !frame->height) {
return;
}
auto frameSize = CGSizeMake(frame->width, frame->height);
auto rotation = 0;
if (frame->data[3] != NULL && (CVPixelBufferRef)frame->data[3]) {
[view renderWithPixelBuffer:(CVPixelBufferRef)frame->data[3]
size: frameSize
rotation: rotation
fillFrame: true];
return;
}
else if (CVPixelBufferRef pixelBuffer = [self getBufferForPreviewFromFrame:frame]) {
[view renderWithPixelBuffer: pixelBuffer
size: frameSize
rotation: rotation
fillFrame: true];
}
}
}
-(void) renderer: (const lrc::api::video::Renderer*)renderer renderFrameForDistantView:(CallMTKView*) view
{
@autoreleasepool {
auto framePtr = renderer->currentAVFrame();
auto frame = framePtr.get();
if(!frame || !frame->width || !frame->height) {
return;
}
auto frameSize = CGSizeMake(frame->width, frame->height);
auto rotation = 0;
if (auto matrix = av_frame_get_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX)) {
const int32_t* data = reinterpret_cast<int32_t*>(matrix->data);
rotation = av_display_rotation_get(data);
}
if (frame->data[3] != NULL && (CVPixelBufferRef)frame->data[3]) {
[view renderWithPixelBuffer: (CVPixelBufferRef)frame->data[3]
size: frameSize
rotation: rotation
fillFrame: false];
}
if (CVPixelBufferRef pixelBuffer = [self getBufferForDistantViewFromFrame:frame]) {
[view renderWithPixelBuffer: pixelBuffer
size: frameSize
rotation: rotation
fillFrame: false];
}
}
}
-(CVPixelBufferRef) getBufferForPreviewFromFrame:(const AVFrame*)frame {
[VideoCommon fillPixelBuffr:&pixelBufferPreview fromFrame:frame bufferPool:&pixelBufferPoolPreview];
CVPixelBufferRef buffer = pixelBufferPreview;
return buffer;
}
-(CVPixelBufferRef) getBufferForDistantViewFromFrame:(const AVFrame*)frame {
[VideoCommon fillPixelBuffr:&pixelBufferDistantView fromFrame:frame bufferPool:&pixelBufferPoolDistantView];
CVPixelBufferRef buffer = pixelBufferDistantView;
return buffer;
}
- (void) initFrame
{
[self.view setFrame:self.view.superview.bounds];
[self.view setHidden:YES];
self.view.layer.position = self.view.frame.origin;
[self collapseRightView];
}
# pragma private IN/OUT animations
-(void)uncollapseRightView
{
NSView *left = [[splitView subviews] objectAtIndex:0];
NSView *right = [[splitView subviews] objectAtIndex:1];
[right setHidden:NO];
CGFloat dividerThickness = [splitView dividerThickness];
// get the different frames
NSRect leftFrame = [left frame];
NSRect rightFrame = [right frame];
leftFrame.size.width = (leftFrame.size.width - rightFrame.size.width - dividerThickness);
rightFrame.origin.x = leftFrame.size.width + dividerThickness;
[left setFrameSize:leftFrame.size];
[right setFrame:rightFrame];
[splitView display];
[self.chatVC takeFocus];
}
-(void) cleanUp
{
if(self.splitView.isInFullScreenMode)
[self.splitView exitFullScreenModeWithOptions:nil];
QObject::disconnect(renderConnections.frameUpdated);
QObject::disconnect(renderConnections.started);
QObject::disconnect(renderConnections.stopped);
QObject::disconnect(self.messageConnection);
[self.chatButton setPressed:NO];
[self collapseRightView];
[timeSpentLabel setStringValue:@""];
[contactIdLabel setStringValue:@""];
[contactNameLabel setStringValue:@""];
[contactPhoto setImage:nil];
//background view
[bluerBackgroundEffect setHidden:NO];
[bluerBackgroundEffect setFillColor:[NSColor darkGrayColor]];
[bluerBackgroundEffect setAlphaValue:0.6];
[backgroundImage setHidden:NO];
backgroundImage.layer.contents = nil;
[self.previewView setHidden:YES];
[self.videoMTKView setHidden:YES];
contactNameLabel.textColor = [NSColor highlightColor];
contactNameLabel.textColor = [NSColor highlightColor];
contactIdLabel.textColor = [NSColor highlightColor];
callStateLabel.textColor = [NSColor highlightColor];
}
-(void) setupCallView
{
if (accountInfo_ == nil)
return;
auto* callModel = accountInfo_->callModel.get();
auto* convModel = accountInfo_->conversationModel.get();
// when call comes in we want to show the controls/header
[self mouseIsMoving: YES];
[videoView setShouldAcceptInteractions: NO];
[self connectVideoSignals];
/* check if text media is already present */
if(not callModel->hasCall(callUid_))
return;
[loadingIndicator setAnimates:YES];
[self updateCall];
/* monitor media for messaging text messaging */
QObject::disconnect(self.messageConnection);
self.messageConnection = QObject::connect(convModel,
&lrc::api::ConversationModel::interactionStatusUpdated,
[self] (std::string convUid,
uint64_t msgId,
lrc::api::interaction::Info msg) {
if (msg.type == lrc::api::interaction::Type::TEXT) {
if(not [[self splitView] isSubviewCollapsed:[[[self splitView] subviews] objectAtIndex: 1]]){
return;
}
[self uncollapseRightView];
}
});
//monitor for updated profile
QObject::disconnect(self.profileUpdatedConnection);
self.profileUpdatedConnection =
QObject::connect(accountInfo_->contactModel.get(),
&lrc::api::ContactModel::contactAdded,
[self](const std::string &contactUri) {
auto convIt = getConversationFromUid(convUid_, *accountInfo_->conversationModel.get());
if (convIt == accountInfo_->conversationModel->allFilteredConversations().end()) {
return;
}
if (convIt->participants.empty()) {
return;
}
auto& contact = accountInfo_->contactModel->getContact(convIt->participants[0]);
if (contact.profileInfo.type == lrc::api::profile::Type::RING && contact.profileInfo.uri == contactUri)
accountInfo_->conversationModel->makePermanent(convUid_);
[contactPhoto setImage: [self getContactImageOfSize:120.0 withDefaultAvatar:YES]];
[self.delegate conversationInfoUpdatedFor:convUid_];
[self setBackground];
});
}
-(void) showWithAnimation:(BOOL)animate
{
if (!animate) {
[self.view setHidden:NO];
[self setupCallView];
return;
}
CGRect frame = CGRectOffset(self.view.superview.bounds, -self.view.superview.bounds.size.width, 0);
[self.view setHidden:NO];
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
[animation setFromValue:[NSValue valueWithPoint:frame.origin]];
[animation setToValue:[NSValue valueWithPoint:self.view.superview.bounds.origin]];
[animation setDuration:0.2f];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]];
[CATransaction setCompletionBlock:^{
[self setupCallView];
}];
[self.view.layer addAnimation:animation forKey:animation.keyPath];
[CATransaction commit];
}
-(void) hideWithAnimation:(BOOL)animate
{
if(self.view.frame.origin.x < 0) {
return;
}
if (!animate) {
[self.view setHidden:YES];
return;
}
CGRect frame = CGRectOffset(self.view.frame, -self.view.frame.size.width, 0);
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
[animation setFromValue:[NSValue valueWithPoint:self.view.frame.origin]];
[animation setToValue:[NSValue valueWithPoint:frame.origin]];
[animation setDuration:0.2f];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[CATransaction setCompletionBlock:^{
[self.view setHidden:YES];
// first make sure everything is disconnected
[self cleanUp];
// if (currentCall_) {
// [self animateIn];
// }
}];
[self.view.layer addAnimation:animation forKey:animation.keyPath];
[self.view.layer setPosition:frame.origin];
[CATransaction commit];
}
/**
* Debug purpose
*/
-(void) dumpFrame:(CGRect) frame WithName:(NSString*) name
{
NSLog(@"frame %@ : %f %f %f %f \n\n",name ,frame.origin.x, frame.origin.y, frame.size.width, frame.size.height);
}
#pragma mark - Button methods
- (IBAction)hangUp:(id)sender {
if (accountInfo_ == nil)
return;
auto* callModel = accountInfo_->callModel.get();
callModel->hangUp(callUid_);
}
- (IBAction)accept:(id)sender {
if (accountInfo_ == nil)
return;
// If we accept a conversation with a non trusted contact, we first accept it
auto convIt = getConversationFromUid(convUid_, *accountInfo_->conversationModel.get());
if (convIt != accountInfo_->conversationModel->allFilteredConversations().end()) {
auto& contact = accountInfo_->contactModel->getContact(convIt->participants[0]);
if (contact.profileInfo.type == lrc::api::profile::Type::PENDING)
accountInfo_->conversationModel->makePermanent(convUid_);
}
auto* callModel = accountInfo_->callModel.get();
callModel->accept(callUid_);
}
- (IBAction)toggleRecording:(id)sender {
if (accountInfo_ == nil)
return;
auto* callModel = accountInfo_->callModel.get();
callModel->toggleAudioRecord(callUid_);
if (callModel->isRecording(callUid_)) {
[recordOnOffButton startBlinkAnimationfrom:[NSColor buttonBlinkColorColor] to:[NSColor whiteColor] scaleFactor: 1 duration: 1.5];
} else {
[recordOnOffButton stopBlinkAnimation];
}
}
- (IBAction)toggleHold:(id)sender {
if (accountInfo_ == nil)
return;
auto* callModel = accountInfo_->callModel.get();
auto currentCall = callModel->getCall(callUid_);
callModel->togglePause(callUid_);
}
- (IBAction)showDialpad:(id)sender {
AppDelegate* appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
[appDelegate showDialpad];
}
-(IBAction)toggleChat:(id)sender;
{
BOOL rightViewCollapsed = [[self splitView] isSubviewCollapsed:[[[self splitView] subviews] objectAtIndex: 1]];
if (rightViewCollapsed) {
[self uncollapseRightView];
} else {
[self collapseRightView];
}
[chatButton setPressed:rightViewCollapsed];
}
- (IBAction)muteAudio:(id)sender
{
if (accountInfo_ == nil)
return;
auto* callModel = accountInfo_->callModel.get();
auto currentCall = callModel->getCall(callUid_);
if (currentCall.audioMuted) {
muteAudioButton.image = [NSImage imageNamed:@"ic_action_audio.png"];
} else {
muteAudioButton.image = [NSImage imageNamed:@"ic_action_mute_audio.png"];
}
callModel->toggleMedia(callUid_, lrc::api::NewCallModel::Media::AUDIO);
}
- (IBAction)muteVideo:(id)sender
{
if (accountInfo_ == nil)
return;
auto* callModel = accountInfo_->callModel.get();
auto currentCall = callModel->getCall(callUid_);
if(!currentCall.isAudioOnly) {
if (currentCall.videoMuted) {
muteVideoButton.image = [NSImage imageNamed:@"ic_action_video.png"];
} else {
muteVideoButton.image = [NSImage imageNamed:@"ic_action_mute_video.png"];
}
}
callModel->toggleMedia(callUid_, lrc::api::NewCallModel::Media::VIDEO);
}
- (IBAction)toggleTransferView:(id)sender {
}
- (IBAction)toggleAddParticipantView:(id)sender {
}
- (IBAction)hidePreview:(id)sender {
CGRect previewFrame = previewView.frame;
CGRect newPreviewFrame, bcHidePreviewFrame;
if (previewFrame.size.width > HIDE_PREVIEW_BUTTON_MAX_SIZE) {
self.movableBaseForView.movable = false;
newPreviewFrame = self.getVideoPreviewCollapsedSize;
bcHidePreviewFrame = [self frameForExpendPreviewButton: true];
hidePreviewButton.image = [NSImage imageNamed: NSImageNameTouchBarEnterFullScreenTemplate];
} else {
self.movableBaseForView.movable = true;
newPreviewFrame = CGRectMake(0, 0, PREVIEW_WIDTH, PREVIEW_HEIGHT);
bcHidePreviewFrame = [self frameForExpendPreviewButton: false];
hidePreviewButton.image = [NSImage imageNamed: NSImageNameTouchBarExitFullScreenTemplate];
}
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
context.duration = 0.2f;
context.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
previewView.animator.frame = newPreviewFrame;
} completionHandler: nil];
hidePreviewBackground.frame = bcHidePreviewFrame;
}
#pragma mark - NSSplitViewDelegate
/* Return YES if the subview should be collapsed because the user has double-clicked on an adjacent divider. If a split view has a delegate, and the delegate responds to this message, it will be sent once for the subview before a divider when the user double-clicks on that divider, and again for the subview after the divider, but only if the delegate returned YES when sent -splitView:canCollapseSubview: for the subview in question. When the delegate indicates that both subviews should be collapsed NSSplitView's behavior is undefined.
*/
- (BOOL)splitView:(NSSplitView *)splitView shouldCollapseSubview:(NSView *)subview forDoubleClickOnDividerAtIndex:(NSInteger)dividerIndex;
{
NSView* rightView = [[splitView subviews] objectAtIndex:1];
return ([subview isEqual:rightView]);
}
- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview;
{
NSView* rightView = [[splitView subviews] objectAtIndex:1];
return ([subview isEqual:rightView]);
}
# pragma mark - CallnDelegate
- (void) callShouldToggleFullScreen
{
if(self.splitView.isInFullScreenMode)
[self.splitView exitFullScreenModeWithOptions:nil];
else {
NSApplicationPresentationOptions options = NSApplicationPresentationDefault +NSApplicationPresentationAutoHideDock +
NSApplicationPresentationAutoHideMenuBar + NSApplicationPresentationAutoHideToolbar;
NSDictionary *opts = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithUnsignedInt:options],
NSFullScreenModeApplicationPresentationOptions, nil];
[self.splitView enterFullScreenMode:[NSScreen mainScreen] withOptions:opts];
}
}
-(void) mouseIsMoving:(BOOL) move
{
[[controlsPanel animator] setAlphaValue:move]; // fade out
[[headerContainer animator] setAlphaValue:move];
}
- (BOOL)splitView:(NSSplitView *)splitView shouldHideDividerAtIndex:(NSInteger)dividerIndex
{
return YES;
}
-(void) screenShare {
NSScreen *mainScreen = [NSScreen mainScreen];
NSRect screenFrame = mainScreen.frame;
QRect captureRect = QRect(screenFrame.origin.x, screenFrame.origin.y, screenFrame.size.width, screenFrame.size.height);
mediaModel->setDisplay(0, screenFrame.origin.x, screenFrame.origin.y, screenFrame.size.width, screenFrame.size.height);
}
-(void) switchToDevice:(int)deviceID {
auto devices = mediaModel->getDevices();
auto device = devices[deviceID];
mediaModel->switchInputTo(device);
}
-(std::vector<std::string>) getDeviceList {
return mediaModel->getDevices();
}
-(void) switchToFile:(std::string)uri {
mediaModel->setInputFile(QUrl::fromLocalFile(uri.c_str()).toLocalFile().toStdString());
}
-(CGRect) getVideoPreviewCollapsedSize {
CGPoint origin;
switch (movableBaseForView.closestCorner) {
case TOP_LEFT:
origin = CGPointMake(0, movableBaseForView.frame.size.height - HIDE_PREVIEW_BUTTON_MAX_SIZE);
break;
case BOTTOM_LEFT:
origin = CGPointMake(0, 0);
break;
case TOP_RIGHT:
origin = CGPointMake(movableBaseForView.frame.size.width - HIDE_PREVIEW_BUTTON_MAX_SIZE, movableBaseForView.frame.size.height - HIDE_PREVIEW_BUTTON_MAX_SIZE);
break;
case BOTTOM_RIGHT:
origin = CGPointMake(movableBaseForView.frame.size.width - HIDE_PREVIEW_BUTTON_MAX_SIZE, 0);
break;
}
return CGRectMake(origin.x, origin.y, HIDE_PREVIEW_BUTTON_MAX_SIZE, HIDE_PREVIEW_BUTTON_MAX_SIZE);
}
-(CGRect) frameForExpendPreviewButton:(BOOL)collapsed {
CGFloat size = collapsed ? HIDE_PREVIEW_BUTTON_MAX_SIZE : HIDE_PREVIEW_BUTTON_MIN_SIZE;
CGPoint origin = CGPointMake(self.previewView.frame.size.width - size,
self.previewView.frame.size.height - size);
return CGRectMake(origin.x, origin.y, size, size);
}
@end