multi-device: add ability to export Ring account

This patch adds a Devices panel for Ring accounts.
This panel contains the list of devices linked with this Ring account,
and the possibility to export the Ring account on the DHT to setup a new
device

Change-Id: I7281b03d4376fbfc2d74c4e520b8cd0726b9166d
Tuleap: #959
diff --git a/src/AccDevicesVC.h b/src/AccDevicesVC.h
new file mode 100644
index 0000000..3ae4c2d
--- /dev/null
+++ b/src/AccDevicesVC.h
@@ -0,0 +1,26 @@
+/*
+ *  Copyright (C) 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 <Cocoa/Cocoa.h>
+#import <account.h>
+@interface AccDevicesVC : NSViewController
+
+@property Account* account;
+
+@end
diff --git a/src/AccDevicesVC.mm b/src/AccDevicesVC.mm
new file mode 100644
index 0000000..a712e88
--- /dev/null
+++ b/src/AccDevicesVC.mm
@@ -0,0 +1,134 @@
+/*
+ *  Copyright (C) 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 "AccDevicesVC.h"
+
+//Qt
+#import <qitemselectionmodel.h>
+
+//LRC
+#import <accountmodel.h>
+#import <ringdevicemodel.h>
+#import <account.h>
+
+#import "QNSTreeController.h"
+#import "ExportPasswordWC.h"
+
+@interface AccDevicesVC () <ExportPasswordDelegate>
+
+@property QNSTreeController* devicesTreeController;
+@property ExportPasswordWC* passwordWC;
+
+@property (unsafe_unretained) IBOutlet NSOutlineView* deviceDetailsView;
+
+@end
+
+@implementation AccDevicesVC
+
+@synthesize passwordWC;
+
+NSInteger const TAG_NAME        =   100;
+NSInteger const TAG_DEVICE_IDS  =   200;
+
+- (void)awakeFromNib
+{
+    NSLog(@"INIT Devices VC");
+
+    QObject::connect(AccountModel::instance().selectionModel(),
+                     &QItemSelectionModel::currentChanged,
+                     [=](const QModelIndex &current, const QModelIndex &previous) {
+                         if(!current.isValid())
+                             return;
+                         [self loadAccount];
+                     });
+}
+
+
+- (void)loadAccount
+{
+    auto account = AccountModel::instance().selectedAccount();
+    self.devicesTreeController = [[QNSTreeController alloc] initWithQModel:(QAbstractItemModel*)account->ringDeviceModel()];
+    [self.devicesTreeController setAvoidsEmptySelection:NO];
+    [self.devicesTreeController setChildrenKeyPath:@"children"];
+
+    [self.deviceDetailsView bind:@"content" toObject:self.devicesTreeController withKeyPath:@"arrangedObjects" options:nil];
+    [self.deviceDetailsView bind:@"sortDescriptors" toObject:self.devicesTreeController withKeyPath:@"sortDescriptors" options:nil];
+    [self.deviceDetailsView bind:@"selectionIndexPaths" toObject:self.devicesTreeController withKeyPath:@"selectionIndexPaths" options:nil];
+}
+
+- (IBAction)startExportOnRing:(id)sender
+{
+    NSButton* btbAdd = (NSButton *) sender;
+
+    self.account = AccountModel::instance().selectedAccount();
+    [self showPasswordPrompt];
+}
+#pragma mark - Export methods
+
+- (void)showPasswordPrompt
+{
+    auto account = AccountModel::instance().selectedAccount();
+    passwordWC = [[ExportPasswordWC alloc] initWithDelegate:self actionCode:1];
+    [passwordWC setAccount: account];
+#if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_9
+    [self.view.window beginSheet:passwordWC.window completionHandler:nil];
+#else
+    [NSApp beginSheet: passwordWC.window
+       modalForWindow: self.view.window
+        modalDelegate: self
+       didEndSelector: nil
+          contextInfo: nil];
+#endif
+}
+
+
+#pragma mark - NSOutlineViewDelegate methods
+
+- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item
+{
+    return YES;
+}
+
+- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
+{
+    return [outlineView makeViewWithIdentifier:@"HoverRowView" owner:nil];
+}
+
+- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
+{
+    NSTableView* result = [outlineView makeViewWithIdentifier:@"DeviceView" owner:self];
+
+    QModelIndex qIdx = [self.devicesTreeController toQIdx:((NSTreeNode*)item)];
+    if(!qIdx.isValid())
+        return result;
+
+    NSTextField* nameLabel = [result viewWithTag:TAG_NAME];
+    NSTextField* deviceIDLabel = [result viewWithTag:TAG_DEVICE_IDS];
+
+    auto account = AccountModel::instance().selectedAccount();
+
+    NSString* string = account->ringDeviceModel()->data(qIdx,Qt::DisplayRole).toString().toNSString();
+
+    [nameLabel setStringValue:account->alias().toNSString()];
+    [deviceIDLabel setStringValue:string];
+
+    return result;
+}
+
+@end
diff --git a/src/AccountsVC.mm b/src/AccountsVC.mm
index c6d14e5..1557915 100644
--- a/src/AccountsVC.mm
+++ b/src/AccountsVC.mm
@@ -36,6 +36,7 @@
 #import "AccAdvancedVC.h"
 #import "AccSecurityVC.h"
 #import "AccRingVC.h"
+#import "AccDevicesVC.h"
 #import "PathPasswordWC.h"
 
 @interface AccountsVC () <PathPasswordDelegate>
@@ -48,6 +49,7 @@
 @property (retain) IBOutlet NSTabViewItem *advancedTabItem;
 @property (retain) IBOutlet NSTabViewItem *securityTabItem;
 @property (retain) IBOutlet NSTabViewItem *ringTabItem;
+@property (retain) IBOutlet NSTabViewItem *ringDevicesTabItem;
 
 @property QNSTreeController *treeController;
 @property (assign) IBOutlet NSOutlineView *accountsListView;
@@ -55,6 +57,7 @@
 @property (unsafe_unretained) IBOutlet NSButton* exportAccountButton;
 
 @property AccRingVC* ringVC;
+@property AccDevicesVC* devicesVC;
 @property AccGeneralVC* generalVC;
 @property AccMediaVC* audioVC;
 @property AccAdvancedVC* advancedVC;
@@ -71,6 +74,7 @@
 @synthesize advancedTabItem;
 @synthesize securityTabItem;
 @synthesize ringTabItem;
+@synthesize ringDevicesTabItem;
 @synthesize accountsListView;
 @synthesize accountDetailsView;
 @synthesize treeController;
@@ -153,6 +157,11 @@
     [[self.ringVC view] setFrame:[self.ringTabItem.view frame]];
     [[self.ringVC view] setBounds:[self.ringTabItem.view bounds]];
     [self.ringTabItem setView:self.ringVC.view];
+
+    self.devicesVC = [[AccDevicesVC alloc] initWithNibName:@"AccDevices" bundle:nil];
+    [[self.devicesVC view] setFrame:[self.ringDevicesTabItem.view frame]];
+    [[self.devicesVC view] setBounds:[self.ringDevicesTabItem.view bounds]];
+    [self.ringDevicesTabItem setView:self.devicesVC.view];
 }
 
 - (IBAction)addAccount:(id)sender {
@@ -193,8 +202,9 @@
     }
 
     [configPanels insertTabViewItem:ringTabItem atIndex:0];
-    [configPanels insertTabViewItem:mediaTabItem atIndex:1];
-    [configPanels insertTabViewItem:advancedTabItem atIndex:2];
+    [configPanels insertTabViewItem:ringDevicesTabItem atIndex:1];
+    [configPanels insertTabViewItem:mediaTabItem atIndex:2];
+    [configPanels insertTabViewItem:advancedTabItem atIndex:3];
 }
 
 - (IBAction)exportAccount:(id)sender
diff --git a/src/ExportPasswordWC.h b/src/ExportPasswordWC.h
new file mode 100644
index 0000000..3a6560c
--- /dev/null
+++ b/src/ExportPasswordWC.h
@@ -0,0 +1,45 @@
+/*
+ *  Copyright (C) 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 <Cocoa/Cocoa.h>
+
+#import <account.h>
+
+#import "LoadingWCDelegate.h"
+#import "AbstractLoadingWC.h"
+
+@protocol ExportPasswordDelegate <LoadingWCDelegate>
+
+@optional
+- (void)didCompleteWithPin:(NSString*) path Password:(NSString*) password;
+- (void)didStartWithPassword:(NSString*) password;
+
+@end
+
+@interface ExportPasswordWC : AbstractLoadingWC
+
+/**
+ * password string contained in passwordField.
+ * This is a KVO method to bind the text with the OK Button
+ * if password.length is > 0, button is enabled, otherwise disabled
+ */
+@property (retain) NSString* password;
+@property (assign) Account* account;
+
+@end
diff --git a/src/ExportPasswordWC.mm b/src/ExportPasswordWC.mm
new file mode 100644
index 0000000..f9b914c
--- /dev/null
+++ b/src/ExportPasswordWC.mm
@@ -0,0 +1,138 @@
+/*
+ *  Copyright (C) 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 "ExportPasswordWC.h"
+
+//LRC
+#import <account.h>
+
+//Ring
+#import "views/ITProgressIndicator.h"
+@interface ExportPasswordWC() <NSTextFieldDelegate>{
+    __unsafe_unretained IBOutlet NSSecureTextField* passwordField;
+    __unsafe_unretained IBOutlet NSTextField* resultField;
+    __unsafe_unretained IBOutlet NSTextField* errorField;
+    __unsafe_unretained IBOutlet ITProgressIndicator* progressIndicator;
+}
+@end
+
+@implementation ExportPasswordWC {
+    struct {
+        unsigned int didStart:1;
+        unsigned int didComplete:1;
+    } delegateRespondsTo;
+}
+
+@synthesize account;
+QMetaObject::Connection accountConnection;
+
+
+#pragma mark - Initialize
+- (id)initWithDelegate:(id <ExportPasswordDelegate>) del actionCode:(NSInteger) code
+{
+    return [super initWithWindowNibName:@"ExportPasswordWindow" delegate:del actionCode:code];
+}
+
+- (void)windowDidLoad
+{
+    [super windowDidLoad];
+}
+
+- (void)setDelegate:(id <ExportPasswordDelegate>)aDelegate
+{
+    if (super.delegate != aDelegate) {
+        [super setDelegate: aDelegate];
+        delegateRespondsTo.didStart = [aDelegate respondsToSelector:@selector(didStartWithPassword:)];
+        delegateRespondsTo.didComplete = [aDelegate respondsToSelector:@selector(didCompleteWithPin:Password:)];
+    }
+}
+
+- (void)showError:(NSString*) errorMessage
+{
+    [errorField setStringValue:errorMessage];
+    [super showError];
+}
+
+- (void)showLoading
+{
+    [progressIndicator setNumberOfLines:30];
+    [progressIndicator setWidthOfLine:2];
+    [progressIndicator setLengthOfLine:5];
+    [progressIndicator setInnerMargin:20];
+    [super showLoading];
+}
+
+#pragma mark - Events Handlers
+- (IBAction)completeAction:(id)sender
+{
+    // Check to avoid exporting an old account (not supported by daemon)
+    if (account->needsMigration()) {
+        [self showError:NSLocalizedString(@"You have to migrate your account before exporting", @"Error shown to user")];
+    } else {
+        NSString* password = passwordField.stringValue;
+        [self showLoading];
+        QObject::disconnect(accountConnection);
+        accountConnection = QObject::connect(account,
+                                             &Account::exportOnRingEnded,
+                                             [=](Account::ExportOnRingStatus status,const QString &pin) {
+                                                 NSLog(@"Export ended!");
+                                                 switch (status) {
+                                                     case Account::ExportOnRingStatus::SUCCESS:{
+                                                         NSString *nsPin = pin.toNSString();
+                                                         NSLog(@"Export ended with Success, pin is %@",nsPin);
+                                                         [resultField setAttributedStringValue:[self formatPinMessage:nsPin]];
+                                                         [self showFinal];
+                                                     }
+                                                         break;
+                                                     case Account::ExportOnRingStatus::WRONG_PASSWORD:{
+                                                         NSLog(@"Export ended with Wrong Password");
+                                                         [self showError:NSLocalizedString(@"Export ended with Wrong Password", @"Error shown to the user" )];
+                                                     }
+                                                         break;
+                                                     case Account::ExportOnRingStatus::NETWORK_ERROR:{
+                                                         NSLog(@"Export ended with NetworkError!");
+                                                         [self showError:NSLocalizedString(@"A network error occured during the export", @"Error shown to the user" )];
+                                                     }
+                                                         break;
+                                                     default:{
+                                                         NSLog(@"Export ended with Unknown status!");
+                                                         [self showError:NSLocalizedString(@"An error occured during the export", @"Error shown to the user" )];
+                                                     }
+                                                         break;
+                                                 }
+                                             });
+        account->exportOnRing(QString::fromNSString(password));
+    }
+}
+
+//TODO: Move String formatting to a dedicated Utility Classes
+- (NSAttributedString *)formatPinMessage:(NSString*) pin
+{
+    NSMutableAttributedString* hereIsThePin = [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"Your generated pin:","Title shown to user to concat with Pin")];
+    NSMutableAttributedString* thePin = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@\n", pin]];
+    [thePin beginEditing];
+    NSRange range = NSMakeRange(0, [thePin length]);
+    [thePin addAttribute:NSFontAttributeName value:[NSFont fontWithName:@"Helvetica-Bold" size:12.0] range:range];
+    [hereIsThePin appendAttributedString:thePin];
+    NSMutableAttributedString* infos = [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"This pin and the account password should be entered on your new device within 5 minutes. On most client, this is done from \"Existing Ring account\" menu. You may generate a new pin at any moment.","Infos on how to use the pin")];
+    [hereIsThePin appendAttributedString:infos];
+
+    return hereIsThePin;
+}
+
+@end