Statistics
| Branch: | Revision:

root / trunk / Pithos.Client.WPF / Shell / ShellViewModel.cs @ 133f83c2

History | View | Annotate | Download (21.4 kB)

1
using System.Collections.Concurrent;
2
using System.Diagnostics;
3
using System.Diagnostics.Contracts;
4
using System.IO;
5
using System.Net;
6
using System.Reflection;
7
using System.Runtime.InteropServices;
8
using System.ServiceModel;
9
using System.Threading.Tasks;
10
using System.Windows;
11
using Caliburn.Micro;
12
using Hardcodet.Wpf.TaskbarNotification;
13
using Pithos.Client.WPF.Configuration;
14
using Pithos.Client.WPF.FileProperties;
15
using Pithos.Client.WPF.SelectiveSynch;
16
using Pithos.Client.WPF.Services;
17
using Pithos.Client.WPF.Shell;
18
using Pithos.Core;
19
using Pithos.Interfaces;
20
using System;
21
using System.Collections.Generic;
22
using System.Linq;
23
using Pithos.Network;
24
using StatusService = Pithos.Client.WPF.Services.StatusService;
25

    
26
namespace Pithos.Client.WPF {
27
	using System.ComponentModel.Composition;
28

    
29
	
30
	///<summary>
31
	/// The "shell" of the Pithos application displays the taskbar  icon, menu and notifications.
32
	/// The shell also hosts the status service called by shell extensions to retrieve file info
33
	///</summary>
34
	///<remarks>
35
	/// It is a strange "shell" as its main visible element is an icon instead of a window
36
	/// The shell subscribes to the following events:
37
	/// * Notification:  Raised by components that want to notify the user. Usually displayed in a balloon
38
	/// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here
39
	/// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
40
	///</remarks>		
41
	//TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
42
	[Export(typeof(IShell))]
43
	public class ShellViewModel : Screen, IStatusNotification, IShell,
44
		IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
45
	{
46
		//The Status Checker provides the current synch state
47
		//TODO: Could we remove the status checker and use events in its place?
48
		private readonly IStatusChecker _statusChecker;
49
		private readonly IEventAggregator _events;
50

    
51
		public PithosSettings Settings { get; private set; }
52

    
53

    
54
        private readonly ConcurrentDictionary<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, PithosMonitor>();
55
		///<summary>
56
		/// Dictionary of account monitors, keyed by account
57
		///</summary>
58
		///<remarks>
59
		/// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
60
		/// retrieve account and boject info		
61
		///</remarks>
62
		// TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
63
		// TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
64
        public ConcurrentDictionary<string, PithosMonitor> Monitors
65
		{
66
			get { return _monitors; }
67
		}
68

    
69

    
70
	    ///<summary>
71
	    /// The status service is used by Shell extensions to retrieve file status information
72
	    ///</summary>
73
	    //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
74
	    private ServiceHost _statusService;
75

    
76
		//Logging in the Pithos client is provided by log4net
77
		private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos");
78

    
79
		///<summary>
80
		/// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
81
		///</summary>
82
		///<remarks>
83
		/// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
84
		///</remarks>
85
		[ImportingConstructor]		
86
		public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings)
87
		{
88
			try
89
			{
90

    
91
				_windowManager = windowManager;
92
				//CHECK: Caliburn doesn't need explicit command construction
93
				//OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
94
				_statusChecker = statusChecker;
95
				//The event subst
96
				_events = events;
97
				_events.Subscribe(this);
98

    
99
				Settings = settings;
100

    
101
				StatusMessage = "In Synch";
102

    
103
				_accounts.CollectionChanged += (sender, e) =>
104
												   {
105
													   NotifyOfPropertyChange(() => OpenFolderCaption);
106
													   NotifyOfPropertyChange(() => HasAccounts);
107
												   };
108

    
109
			}
110
			catch (Exception exc)
111
			{
112
				Log.Error("Error while starting the ShellViewModel",exc);
113
				throw;
114
			}
115
		}
116

    
117

    
118
		protected override void OnActivate()
119
		{
120
			base.OnActivate();
121

    
122
			StartMonitoring();                    
123
		}
124

    
125

    
126
		private async void StartMonitoring()
127
		{
128
			try
129
			{
130
				var accounts = Settings.Accounts.Select(MonitorAccount);
131
				await TaskEx.WhenAll(accounts);
132
				_statusService = StatusService.Start();
133

    
134
/*
135
				foreach (var account in Settings.Accounts)
136
				{
137
					await MonitorAccount(account);
138
				}
139
*/
140
				
141
			}
142
			catch (AggregateException exc)
143
			{
144
				exc.Handle(e =>
145
				{
146
					Log.Error("Error while starting monitoring", e);
147
					return true;
148
				});
149
				throw;
150
			}
151
		}
152

    
153
		protected override void OnDeactivate(bool close)
154
		{
155
			base.OnDeactivate(close);
156
			if (close)
157
			{
158
				StatusService.Stop(_statusService);
159
				_statusService = null;
160
			}
161
		}
162

    
163
		public Task MonitorAccount(AccountSettings account)
164
		{
165
			return Task.Factory.StartNew(() =>
166
			{                                                
167
				PithosMonitor monitor;
168
				var accountName = account.AccountName;
169

    
170
				if (_monitors.TryGetValue(accountName, out monitor))
171
				{
172
					//If the account is active
173
					if (account.IsActive)
174
						//Start the monitor. It's OK to start an already started monitor,
175
						//it will just ignore the call                        
176
						StartMonitor(monitor).Wait();                        
177
					else
178
					{
179
						//If the account is inactive
180
						//Stop and remove the monitor
181
						RemoveMonitor(accountName);
182
					}
183
					return;
184
				}
185

    
186
				//Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
187
				monitor = new PithosMonitor
188
							  {
189
								  UserName = accountName,
190
								  ApiKey = account.ApiKey,                                  
191
								  StatusNotification = this,
192
								  RootPath = account.RootPath
193
							  };
194
				//PithosMonitor uses MEF so we need to resolve it
195
				IoC.BuildUp(monitor);
196

    
197
			    monitor.AuthenticationUrl = account.ServerUrl;
198

    
199
				_monitors[accountName] = monitor;
200

    
201
				if (account.IsActive)
202
				{
203
					//Don't start a monitor if it doesn't have an account and ApiKey
204
					if (String.IsNullOrWhiteSpace(monitor.UserName) ||
205
						String.IsNullOrWhiteSpace(monitor.ApiKey))
206
						return;
207
					StartMonitor(monitor);
208
				}
209
			});
210
		}
211

    
212

    
213
		protected override void OnViewLoaded(object view)
214
		{
215
			UpdateStatus();
216
			var window = (Window)view;            
217
			TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
218
			base.OnViewLoaded(view);
219
		}
220

    
221

    
222
		#region Status Properties
223

    
224
		private string _statusMessage;
225
		public string StatusMessage
226
		{
227
			get { return _statusMessage; }
228
			set
229
			{
230
				_statusMessage = value;
231
				NotifyOfPropertyChange(() => StatusMessage);
232
			}
233
		}
234

    
235
		private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
236
		public ObservableConcurrentCollection<AccountInfo> Accounts
237
		{
238
			get { return _accounts; }
239
		}
240

    
241
		public bool HasAccounts
242
		{
243
			get { return _accounts.Count > 0; }
244
		}
245

    
246

    
247
		public string OpenFolderCaption
248
		{
249
			get
250
			{
251
				return (_accounts.Count == 0)
252
						? "No Accounts Defined"
253
						: "Open Pithos Folder";
254
			}
255
		}
256

    
257
		private string _pauseSyncCaption="Pause Synching";
258
		public string PauseSyncCaption
259
		{
260
			get { return _pauseSyncCaption; }
261
			set
262
			{
263
				_pauseSyncCaption = value;
264
				NotifyOfPropertyChange(() => PauseSyncCaption);
265
			}
266
		}
267

    
268
		private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
269
		public ObservableConcurrentCollection<FileEntry> RecentFiles
270
		{
271
			get { return _recentFiles; }
272
		}
273

    
274

    
275
		private string _statusIcon="../Images/Pithos.ico";
276
		public string StatusIcon
277
		{
278
			get { return _statusIcon; }
279
			set
280
			{
281
                //TODO: Ensure all status icons use the Pithos logo
282
				_statusIcon = value;
283
				NotifyOfPropertyChange(() => StatusIcon);
284
			}
285
		}
286

    
287
		#endregion
288

    
289
		#region Commands
290

    
291
		public void ShowPreferences()
292
		{
293
			Settings.Reload();
294
			var preferences = new PreferencesViewModel(_windowManager,_events, this,Settings);            
295
			_windowManager.ShowDialog(preferences);
296
			
297
		}
298

    
299
		public void AboutPithos()
300
		{
301
			var about = new AboutViewModel();
302
			_windowManager.ShowWindow(about);
303
		}
304

    
305
		public void SendFeedback()
306
		{
307
			var feedBack =  IoC.Get<FeedbackViewModel>();
308
			_windowManager.ShowWindow(feedBack);
309
		}
310

    
311
		//public PithosCommand OpenPithosFolderCommand { get; private set; }
312

    
313
		public void OpenPithosFolder()
314
		{
315
			var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
316
			if (account == null)
317
				return;
318
			Process.Start(account.RootPath);
319
		}
320

    
321
		public void OpenPithosFolder(AccountInfo account)
322
		{
323
			Process.Start(account.AccountPath);
324
		}
325

    
326
		
327
/*
328
		public void GoToSite()
329
		{            
330
			var site = Properties.Settings.Default.PithosSite;
331
			Process.Start(site);            
332
		}
333
*/
334

    
335
		public void GoToSite(AccountInfo account)
336
		{
337
			/*var site = String.Format("{0}/ui/?token={1}&user={2}",
338
				account.SiteUri,account.Token,
339
				account.UserName);*/
340
			Process.Start(account.SiteUri);
341
		}
342

    
343
		public void ShowFileProperties()
344
		{
345
			var account = Settings.Accounts.First(acc => acc.IsActive);            
346
			var dir = new DirectoryInfo(account.RootPath + @"\pithos");
347
			var files=dir.GetFiles();
348
			var r=new Random();
349
			var idx=r.Next(0, files.Length);
350
			ShowFileProperties(files[idx].FullName);            
351
		}
352

    
353
		public void ShowFileProperties(string filePath)
354
		{
355
			if (String.IsNullOrWhiteSpace(filePath))
356
				throw new ArgumentNullException("filePath");
357
			if (!File.Exists(filePath) && !Directory.Exists(filePath))
358
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
359
			Contract.EndContractBlock();
360

    
361
			var pair=(from monitor in  Monitors
362
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
363
								   select monitor).FirstOrDefault();
364
		    var accountMonitor = pair.Value;
365

    
366
			if (accountMonitor == null)
367
				return;
368

    
369
			var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
370

    
371
			
372

    
373
			var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
374
			_windowManager.ShowWindow(fileProperties);
375
		} 
376
		
377
		public void ShowContainerProperties()
378
		{
379
			var account = Settings.Accounts.First(acc => acc.IsActive);            
380
			var dir = new DirectoryInfo(account.RootPath);
381
			var fullName = (from folder in dir.EnumerateDirectories()
382
							where (folder.Attributes & FileAttributes.Hidden) == 0
383
							select folder.FullName).First();
384
			ShowContainerProperties(fullName);            
385
		}
386

    
387
		public void ShowContainerProperties(string filePath)
388
		{
389
			if (String.IsNullOrWhiteSpace(filePath))
390
				throw new ArgumentNullException("filePath");
391
			if (!Directory.Exists(filePath))
392
				throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
393
			Contract.EndContractBlock();
394

    
395
			var pair=(from monitor in  Monitors
396
							   where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
397
								   select monitor).FirstOrDefault();
398
		    var accountMonitor = pair.Value;            
399
			var info = accountMonitor.GetContainerInfo(filePath);
400

    
401
			
402

    
403
			var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
404
			_windowManager.ShowWindow(containerProperties);
405
		}
406

    
407
        public void SynchNow()
408
        {}
409

    
410
		public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
411
		{
412
			if (currentInfo==null)
413
				throw new ArgumentNullException("currentInfo");
414
			Contract.EndContractBlock();
415

    
416
			var monitor = Monitors[currentInfo.Account];
417
			var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
418
			return newInfo;
419
		}
420

    
421
		public ContainerInfo RefreshContainerInfo(ContainerInfo container)
422
		{
423
			if (container == null)
424
				throw new ArgumentNullException("container");
425
			Contract.EndContractBlock();
426

    
427
			var monitor = Monitors[container.Account];
428
			var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
429
			return newInfo;
430
		}
431

    
432

    
433
		public void ToggleSynching()
434
		{
435
			bool isPaused=false;
436
			foreach (var pair in Monitors)
437
			{
438
				var monitor = pair.Value;
439
				monitor.Pause = !monitor.Pause;
440
				isPaused = monitor.Pause;
441
			}
442

    
443
			PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
444
			var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
445
			StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
446
		}
447

    
448
		public void ExitPithos()
449
		{
450
			foreach (var pair in Monitors)
451
			{
452
				var monitor = pair.Value;
453
				monitor.Stop();
454
			}
455

    
456
			((Window)GetView()).Close();
457
		}
458
		#endregion
459

    
460

    
461
		private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
462
			{
463
				new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
464
				new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"),
465
				new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
466
			}.ToDictionary(s => s.Status);
467

    
468
		readonly IWindowManager _windowManager;
469

    
470

    
471
		///<summary>
472
		/// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat		
473
		///</summary>
474
		public void UpdateStatus()
475
		{
476
			var pithosStatus = _statusChecker.GetPithosStatus();
477

    
478
			if (_iconNames.ContainsKey(pithosStatus))
479
			{
480
				var info = _iconNames[pithosStatus];
481
				StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
482

    
483
				Assembly assembly = Assembly.GetExecutingAssembly();                               
484
				var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
485

    
486

    
487
				StatusMessage = String.Format("Pithos {0}\r\n{1}", fileVersion.FileVersion,info.StatusText);
488
			}
489
			
490
			_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info});
491
		}
492

    
493

    
494
	   
495
		private Task StartMonitor(PithosMonitor monitor,int retries=0)
496
		{
497
			return Task.Factory.StartNew(() =>
498
			{
499
				using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
500
				{
501
					try
502
					{
503
						Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
504

    
505
						monitor.Start();
506
					}
507
					catch (WebException exc)
508
					{
509
						if (AbandonRetry(monitor, retries))
510
							return;
511

    
512
						if (IsUnauthorized(exc))
513
						{
514
							var message = String.Format("API Key Expired for {0}. Starting Renewal",monitor.UserName);                            
515
							Log.Error(message,exc);
516
							TryAuthorize(monitor,retries).Wait();
517
						}
518
						else
519
						{
520
							TryLater(monitor, exc,retries);
521
						}
522
					}
523
					catch (Exception exc)
524
					{
525
						if (AbandonRetry(monitor, retries)) 
526
							return;
527

    
528
						TryLater(monitor,exc,retries);
529
					}
530
				}
531
			});
532
		}
533

    
534
		private bool AbandonRetry(PithosMonitor monitor, int retries)
535
		{
536
			if (retries > 1)
537
			{
538
				var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
539
											monitor.UserName);
540
				_events.Publish(new Notification
541
									{Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
542
				return true;
543
			}
544
			return false;
545
		}
546

    
547

    
548
		private async Task TryAuthorize(PithosMonitor monitor,int retries)
549
		{
550
			_events.Publish(new Notification { Title = "Authorization failed", Message = "Your API Key has probably expired. You will be directed to a page where you can renew it", Level = TraceLevel.Error });
551

    
552
			try
553
			{
554

    
555
				var credentials = await PithosAccount.RetrieveCredentials(Settings.PithosLoginUrl);
556

    
557
				var account = Settings.Accounts.First(act => act.AccountName == credentials.UserName);
558
				account.ApiKey = credentials.Password;
559
				monitor.ApiKey = credentials.Password;
560
				Settings.Save();
561
				await TaskEx.Delay(10000);
562
				StartMonitor(monitor, retries + 1);
563
				NotifyOfPropertyChange(()=>Accounts);
564
			}
565
			catch (AggregateException exc)
566
			{
567
				string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
568
				Log.Error(message, exc.InnerException);
569
				_events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
570
			}
571
			catch (Exception exc)
572
			{
573
				string message = String.Format("API Key retrieval for {0} failed", monitor.UserName);
574
				Log.Error(message, exc);
575
				_events.Publish(new Notification { Title = "Authorization failed", Message = message, Level = TraceLevel.Error });
576
			}
577

    
578
		}
579

    
580
		private static bool IsUnauthorized(WebException exc)
581
		{
582
			if (exc==null)
583
				throw new ArgumentNullException("exc");
584
			Contract.EndContractBlock();
585

    
586
			var response = exc.Response as HttpWebResponse;
587
			if (response == null)
588
				return false;
589
			return (response.StatusCode == HttpStatusCode.Unauthorized);
590
		}
591

    
592
		private void TryLater(PithosMonitor monitor, Exception exc,int retries)
593
		{
594
			var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
595
			Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
596
			_events.Publish(new Notification
597
								{Title = "Error", Message = message, Level = TraceLevel.Error});
598
			Log.Error(message, exc);
599
		}
600

    
601

    
602
		public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
603
		{
604
			StatusMessage = status;
605
			
606
			_events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
607
		}
608

    
609
		public void NotifyChangedFile(string filePath)
610
		{
611
			var entry = new FileEntry {FullPath=filePath};
612
			IProducerConsumerCollection<FileEntry> files=RecentFiles;
613
			FileEntry popped;
614
			while (files.Count > 5)
615
				files.TryTake(out popped);
616
			files.TryAdd(entry);
617
		}
618

    
619
		public void NotifyAccount(AccountInfo account)
620
		{
621
			if (account== null)
622
				return;
623
			//TODO: What happens to an existing account whose Token has changed?
624
			account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
625
				account.SiteUri, Uri.EscapeUriString(account.Token),
626
				Uri.EscapeUriString(account.UserName));
627

    
628
			if (Accounts.All(item => item.UserName != account.UserName))
629
				Accounts.TryAdd(account);
630

    
631
		}
632

    
633

    
634
		public void RemoveMonitor(string accountName)
635
		{
636
			if (String.IsNullOrWhiteSpace(accountName))
637
				return;
638

    
639
			var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
640
			_accounts.TryRemove(accountInfo);
641

    
642
			PithosMonitor monitor;
643
			if (Monitors.TryRemove(accountName, out monitor))
644
			{
645
				monitor.Stop();
646
			}
647
		}
648

    
649
		public void RefreshOverlays()
650
		{
651
			foreach (var pair in Monitors)
652
			{
653
				var monitor = pair.Value;
654

    
655
				var path = monitor.RootPath;
656

    
657
				if (String.IsNullOrWhiteSpace(path))
658
					continue;
659

    
660
				if (!Directory.Exists(path) && !File.Exists(path))
661
					continue;
662

    
663
				IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
664

    
665
				try
666
				{
667
					NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
668
												 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
669
												 pathPointer, IntPtr.Zero);
670
				}
671
				finally
672
				{
673
					Marshal.FreeHGlobal(pathPointer);
674
				}
675
			}
676
		}
677

    
678
		#region Event Handlers
679
		
680
		public void Handle(SelectiveSynchChanges message)
681
		{
682
			var accountName = message.Account.AccountName;
683
			PithosMonitor monitor;
684
			if (_monitors.TryGetValue(accountName, out monitor))
685
			{
686
				monitor.AddSelectivePaths(message.Added);
687
				monitor.RemoveSelectivePaths(message.Removed);
688

    
689
			}
690
			
691
		}
692

    
693

    
694
		public void Handle(Notification notification)
695
		{
696
			if (!Settings.ShowDesktopNotifications)
697
				return;
698
			BalloonIcon icon;
699
			switch (notification.Level)
700
			{
701
				case TraceLevel.Error:
702
					icon = BalloonIcon.Error;
703
					break;
704
				case TraceLevel.Info:
705
				case TraceLevel.Verbose:
706
					icon = BalloonIcon.Info;
707
					break;
708
				case TraceLevel.Warning:
709
					icon = BalloonIcon.Warning;
710
					break;
711
				default:
712
					icon = BalloonIcon.None;
713
					break;
714
			}
715

    
716
			if (Settings.ShowDesktopNotifications)
717
			{
718
				var tv = (ShellView) GetView();
719
				tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon);
720
			}
721
		}
722
		#endregion
723

    
724
		public void Handle(ShowFilePropertiesEvent message)
725
		{
726
			if (message == null)
727
				throw new ArgumentNullException("message");
728
			if (String.IsNullOrWhiteSpace(message.FileName) )
729
				throw new ArgumentException("message");
730
			Contract.EndContractBlock();
731

    
732
			var fileName = message.FileName;
733
            //TODO: Display file properties for non-container folders
734
			if (File.Exists(fileName))
735
                //Retrieve the full name with exact casing. Pithos names are case sensitive				
736
                ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
737
			else if (Directory.Exists(fileName))
738
                //Retrieve the full name with exact casing. Pithos names are case sensitive
739
			{
740
                var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
741
                if (IsContainer(path))
742
			        ShowContainerProperties(path);
743
                else
744
                    ShowFileProperties(path);
745
			}
746
		}
747

    
748
	    private bool IsContainer(string path)
749
	    {
750
	        var matchingFolders = from account in _accounts
751
	                              from rootFolder in Directory.GetDirectories(account.AccountPath)
752
	                              where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
753
	                              select rootFolder;
754
	        return matchingFolders.Any();
755
	    }
756

    
757
	}
758
}