2 /* -----------------------------------------------------------------------
3 * <copyright file="ShellViewModel.cs" company="GRNet">
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
11 * 1. Redistributions of source code must retain the above
12 * copyright notice, this list of conditions and the following
15 * 2. Redistributions in binary form must reproduce the above
16 * copyright notice, this list of conditions and the following
17 * disclaimer in the documentation and/or other materials
18 * provided with the distribution.
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
34 * The views and conclusions contained in the software and
35 * documentation are those of the authors and should not be
36 * interpreted as representing official policies, either expressed
37 * or implied, of GRNET S.A.
39 * -----------------------------------------------------------------------
42 using System.Collections.Concurrent;
43 using System.Diagnostics;
44 using System.Diagnostics.Contracts;
47 using System.Reflection;
48 using System.Runtime.InteropServices;
49 using System.ServiceModel;
50 using System.Threading.Tasks;
52 using System.Windows.Controls.Primitives;
53 using AppLimit.NetSparkle;
55 using Hardcodet.Wpf.TaskbarNotification;
56 using Pithos.Client.WPF.Configuration;
57 using Pithos.Client.WPF.FileProperties;
58 using Pithos.Client.WPF.Preferences;
59 using Pithos.Client.WPF.SelectiveSynch;
60 using Pithos.Client.WPF.Services;
61 using Pithos.Client.WPF.Shell;
63 using Pithos.Core.Agents;
64 using Pithos.Interfaces;
66 using System.Collections.Generic;
69 using StatusService = Pithos.Client.WPF.Services.StatusService;
71 namespace Pithos.Client.WPF {
72 using System.ComponentModel.Composition;
76 /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications.
77 /// The shell also hosts the status service called by shell extensions to retrieve file info
80 /// It is a strange "shell" as its main visible element is an icon instead of a window
81 /// The shell subscribes to the following events:
82 /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon
83 /// * 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
84 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
86 //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
87 [Export(typeof(IShell))]
88 public class ShellViewModel : Screen, IStatusNotification, IShell,
89 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
92 //The Status Checker provides the current synch state
93 //TODO: Could we remove the status checker and use events in its place?
94 private readonly IStatusChecker _statusChecker;
95 private readonly IEventAggregator _events;
97 public PithosSettings Settings { get; private set; }
100 private readonly ConcurrentDictionary<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, PithosMonitor>();
102 /// Dictionary of account monitors, keyed by account
105 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
106 /// retrieve account and boject info
108 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
109 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
110 public ConcurrentDictionary<string, PithosMonitor> Monitors
112 get { return _monitors; }
117 /// The status service is used by Shell extensions to retrieve file status information
119 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
120 private ServiceHost _statusService;
122 //Logging in the Pithos client is provided by log4net
123 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
125 //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
126 private readonly Lazy<FileVersionInfo> _fileVersion;
128 private readonly PollAgent _pollAgent;
131 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
134 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
136 [ImportingConstructor]
137 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent)
142 _windowManager = windowManager;
143 //CHECK: Caliburn doesn't need explicit command construction
144 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
145 _statusChecker = statusChecker;
148 _events.Subscribe(this);
150 _pollAgent = pollAgent;
153 Proxy.SetFromSettings(settings);
155 StatusMessage = "In Synch";
157 _fileVersion= new Lazy<FileVersionInfo>(() =>
159 Assembly assembly = Assembly.GetExecutingAssembly();
160 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
163 _accounts.CollectionChanged += (sender, e) =>
165 NotifyOfPropertyChange(() => OpenFolderCaption);
166 NotifyOfPropertyChange(() => HasAccounts);
170 catch (Exception exc)
172 Log.Error("Error while starting the ShellViewModel",exc);
179 protected override void OnActivate()
183 _sparkle = new Sparkle(Settings.UpdateUrl);
184 _sparkle.updateDetected += OnUpgradeDetected;
185 _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
187 //Must delay opening the upgrade window
188 //to avoid Windows Messages sent by the TaskbarIcon
189 TaskEx.Delay(5000).ContinueWith(_=>
190 Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
196 private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
198 Log.InfoFormat("Update detected {0}",e.LatestVersion);
201 public void CheckForUpgrade()
203 Log.Error("Test Error message");
206 _sparkle=new Sparkle(Settings.UpdateUrl);
207 _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
210 private async void StartMonitoring()
214 var accounts = Settings.Accounts.Select(MonitorAccount);
215 await TaskEx.WhenAll(accounts);
216 _statusService = StatusService.Start();
219 foreach (var account in Settings.Accounts)
221 await MonitorAccount(account);
226 catch (AggregateException exc)
230 Log.Error("Error while starting monitoring", e);
237 protected override void OnDeactivate(bool close)
239 base.OnDeactivate(close);
242 StatusService.Stop(_statusService);
243 _statusService = null;
247 public Task MonitorAccount(AccountSettings account)
249 return Task.Factory.StartNew(() =>
251 PithosMonitor monitor;
252 var accountName = account.AccountName;
254 if (_monitors.TryGetValue(accountName, out monitor))
256 //If the account is active
257 if (account.IsActive)
259 //The Api Key may have changed throuth the Preferences dialog
260 monitor.ApiKey = account.ApiKey;
261 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
262 monitor.RootPath = account.RootPath;
263 //Start the monitor. It's OK to start an already started monitor,
264 //it will just ignore the call
265 StartMonitor(monitor).Wait();
269 //If the account is inactive
270 //Stop and remove the monitor
271 RemoveMonitor(accountName);
277 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
278 monitor = new PithosMonitor
280 UserName = accountName,
281 ApiKey = account.ApiKey,
282 StatusNotification = this,
283 RootPath = account.RootPath
285 //PithosMonitor uses MEF so we need to resolve it
286 IoC.BuildUp(monitor);
288 monitor.AuthenticationUrl = account.ServerUrl;
290 _monitors[accountName] = monitor;
292 if (account.IsActive)
294 //Don't start a monitor if it doesn't have an account and ApiKey
295 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
296 String.IsNullOrWhiteSpace(monitor.ApiKey))
298 StartMonitor(monitor);
304 protected override void OnViewLoaded(object view)
307 var window = (Window)view;
308 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
309 base.OnViewLoaded(view);
313 #region Status Properties
315 private string _statusMessage;
316 public string StatusMessage
318 get { return _statusMessage; }
321 _statusMessage = value;
322 NotifyOfPropertyChange(() => StatusMessage);
326 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
327 public ObservableConcurrentCollection<AccountInfo> Accounts
329 get { return _accounts; }
332 public bool HasAccounts
334 get { return _accounts.Count > 0; }
338 public string OpenFolderCaption
342 return (_accounts.Count == 0)
343 ? "No Accounts Defined"
344 : "Open Pithos Folder";
348 private string _pauseSyncCaption="Pause Synching";
349 public string PauseSyncCaption
351 get { return _pauseSyncCaption; }
354 _pauseSyncCaption = value;
355 NotifyOfPropertyChange(() => PauseSyncCaption);
359 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
360 public ObservableConcurrentCollection<FileEntry> RecentFiles
362 get { return _recentFiles; }
366 private string _statusIcon="../Images/Pithos.ico";
367 public string StatusIcon
369 get { return _statusIcon; }
372 //TODO: Ensure all status icons use the Pithos logo
374 NotifyOfPropertyChange(() => StatusIcon);
382 public void ShowPreferences()
384 ShowPreferences(null);
387 public void ShowPreferences(string currentTab)
390 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
391 _windowManager.ShowDialog(preferences);
395 public void AboutPithos()
397 var about = new AboutViewModel();
398 _windowManager.ShowWindow(about);
401 public void SendFeedback()
403 var feedBack = IoC.Get<FeedbackViewModel>();
404 _windowManager.ShowWindow(feedBack);
407 //public PithosCommand OpenPithosFolderCommand { get; private set; }
409 public void OpenPithosFolder()
411 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
414 Process.Start(account.RootPath);
417 public void OpenPithosFolder(AccountInfo account)
419 Process.Start(account.AccountPath);
424 public void GoToSite()
426 var site = Properties.Settings.Default.PithosSite;
431 public void GoToSite(AccountInfo account)
433 var uri = account.SiteUri.Replace("http://","https://");
437 public void ShowMiniStatus()
439 var model=IoC.Get<MiniStatusViewModel>();
441 _windowManager.ShowWindow(model);
445 /// Open an explorer window to the target path's directory
446 /// and select the file
448 /// <param name="entry"></param>
449 public void GoToFile(FileEntry entry)
451 var fullPath = entry.FullPath;
452 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
454 Process.Start("explorer.exe","/select, " + fullPath);
457 public void OpenLogPath()
459 var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
460 var pithosDataPath = Path.Combine(appDataPath, "GRNET");
462 Process.Start(pithosDataPath);
465 public void ShowFileProperties()
467 var account = Settings.Accounts.First(acc => acc.IsActive);
468 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
469 var files=dir.GetFiles();
471 var idx=r.Next(0, files.Length);
472 ShowFileProperties(files[idx].FullName);
475 public void ShowFileProperties(string filePath)
477 if (String.IsNullOrWhiteSpace(filePath))
478 throw new ArgumentNullException("filePath");
479 if (!File.Exists(filePath) && !Directory.Exists(filePath))
480 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
481 Contract.EndContractBlock();
483 var pair=(from monitor in Monitors
484 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
485 select monitor).FirstOrDefault();
486 var accountMonitor = pair.Value;
488 if (accountMonitor == null)
491 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
495 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
496 _windowManager.ShowWindow(fileProperties);
499 public void ShowContainerProperties()
501 var account = Settings.Accounts.First(acc => acc.IsActive);
502 var dir = new DirectoryInfo(account.RootPath);
503 var fullName = (from folder in dir.EnumerateDirectories()
504 where (folder.Attributes & FileAttributes.Hidden) == 0
505 select folder.FullName).First();
506 ShowContainerProperties(fullName);
509 public void ShowContainerProperties(string filePath)
511 if (String.IsNullOrWhiteSpace(filePath))
512 throw new ArgumentNullException("filePath");
513 if (!Directory.Exists(filePath))
514 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
515 Contract.EndContractBlock();
517 var pair=(from monitor in Monitors
518 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
519 select monitor).FirstOrDefault();
520 var accountMonitor = pair.Value;
521 var info = accountMonitor.GetContainerInfo(filePath);
525 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
526 _windowManager.ShowWindow(containerProperties);
529 public void SynchNow()
531 _pollAgent.SynchNow();
534 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
536 if (currentInfo==null)
537 throw new ArgumentNullException("currentInfo");
538 Contract.EndContractBlock();
540 var monitor = Monitors[currentInfo.Account];
541 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
545 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
547 if (container == null)
548 throw new ArgumentNullException("container");
549 Contract.EndContractBlock();
551 var monitor = Monitors[container.Account];
552 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
557 public void ToggleSynching()
560 foreach (var pair in Monitors)
562 var monitor = pair.Value;
563 monitor.Pause = !monitor.Pause;
564 isPaused = monitor.Pause;
568 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
569 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
570 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
573 public void ExitPithos()
575 foreach (var pair in Monitors)
577 var monitor = pair.Value;
581 ((Window)GetView()).Close();
586 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
588 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
589 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
590 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
591 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
592 }.ToDictionary(s => s.Status);
594 readonly IWindowManager _windowManager;
596 //private int _syncCount=0;
599 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
601 public void SetPithosStatus(PithosStatus status)
603 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
605 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
607 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
608 _pithosStatus = PithosStatus.InSynch;
610 _pithosStatus = status;
614 public void SetPithosStatus(PithosStatus status,string message)
616 StatusMessage = message;
617 SetPithosStatus(status);
623 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
625 public void UpdateStatus()
628 if (_iconNames.ContainsKey(_pithosStatus))
630 var info = _iconNames[_pithosStatus];
631 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
634 if (_pithosStatus == PithosStatus.InSynch)
635 StatusMessage = "All files up to date";
640 private Task StartMonitor(PithosMonitor monitor,int retries=0)
642 return Task.Factory.StartNew(() =>
644 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
648 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
652 catch (WebException exc)
654 if (AbandonRetry(monitor, retries))
657 HttpStatusCode statusCode =HttpStatusCode.OK;
658 var response = exc.Response as HttpWebResponse;
660 statusCode = response.StatusCode;
664 case HttpStatusCode.Unauthorized:
665 var message = String.Format("API Key Expired for {0}. Starting Renewal",
667 Log.Error(message, exc);
668 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
669 account.IsExpired = true;
670 Notify(new ExpirationNotification(account));
671 //TryAuthorize(monitor.UserName, retries).Wait();
673 case HttpStatusCode.ProxyAuthenticationRequired:
674 TryAuthenticateProxy(monitor,retries);
677 TryLater(monitor, exc, retries);
681 catch (Exception exc)
683 if (AbandonRetry(monitor, retries))
686 TryLater(monitor,exc,retries);
692 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
694 Execute.OnUIThread(() =>
696 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
697 proxyAccount.Settings = Settings;
698 if (true != _windowManager.ShowDialog(proxyAccount))
700 StartMonitor(monitor, retries);
701 NotifyOfPropertyChange(() => Accounts);
705 private bool AbandonRetry(PithosMonitor monitor, int retries)
709 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
711 _events.Publish(new Notification
712 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
719 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
721 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
722 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
723 _events.Publish(new Notification
724 {Title = "Error", Message = message, Level = TraceLevel.Error});
725 Log.Error(message, exc);
729 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
731 StatusMessage = status;
733 _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
736 public void NotifyChangedFile(string filePath)
738 if (RecentFiles.Any(e => e.FullPath == filePath))
741 IProducerConsumerCollection<FileEntry> files=RecentFiles;
743 while (files.Count > 5)
744 files.TryTake(out popped);
745 var entry = new FileEntry { FullPath = filePath };
749 public void NotifyAccount(AccountInfo account)
753 //TODO: What happens to an existing account whose Token has changed?
754 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
755 account.SiteUri, Uri.EscapeDataString(account.Token),
756 Uri.EscapeDataString(account.UserName));
758 if (Accounts.All(item => item.UserName != account.UserName))
759 Accounts.TryAdd(account);
763 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
765 if (conflictFiles == null)
767 //Convert to list to avoid multiple iterations
768 var files = conflictFiles.ToList();
773 //TODO: Create a more specific message. For now, just show a warning
774 NotifyForFiles(files,message,TraceLevel.Warning);
778 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
785 StatusMessage = message;
787 _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
790 public void Notify(Notification notification)
792 _events.Publish(notification);
796 public void RemoveMonitor(string accountName)
798 if (String.IsNullOrWhiteSpace(accountName))
801 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
802 if (accountInfo != null)
804 _accounts.TryRemove(accountInfo);
805 _pollAgent.RemoveAccount(accountInfo);
808 PithosMonitor monitor;
809 if (Monitors.TryRemove(accountName, out monitor))
812 //TODO: Also remove any pending actions for this account
813 //from the network queue
817 public void RefreshOverlays()
819 foreach (var pair in Monitors)
821 var monitor = pair.Value;
823 var path = monitor.RootPath;
825 if (String.IsNullOrWhiteSpace(path))
828 if (!Directory.Exists(path) && !File.Exists(path))
831 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
835 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
836 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
837 pathPointer, IntPtr.Zero);
841 Marshal.FreeHGlobal(pathPointer);
846 #region Event Handlers
848 public void Handle(SelectiveSynchChanges message)
850 var accountName = message.Account.AccountName;
851 PithosMonitor monitor;
852 if (_monitors.TryGetValue(accountName, out monitor))
854 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
861 private bool _pollStarted;
862 private Sparkle _sparkle;
864 //SMELL: Doing so much work for notifications in the shell is wrong
865 //The notifications should be moved to their own view/viewmodel pair
866 //and different templates should be used for different message types
867 //This will also allow the addition of extra functionality, eg. actions
869 public void Handle(Notification notification)
873 if (!Settings.ShowDesktopNotifications)
876 if (notification is PollNotification)
881 if (notification is CloudNotification)
886 notification.Title = "Pithos";
887 notification.Message = "Start Synchronisation";
890 var deleteNotification = notification as CloudDeleteNotification;
891 if (deleteNotification != null)
893 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
897 var progress = notification as ProgressNotification;
898 if (progress != null)
900 StatusMessage = String.Format("Pithos {0}\r\n{1} {2:p2} of {3} - {4}",
901 _fileVersion.Value.FileVersion,
903 progress.Block/(double)progress.TotalBlocks,
904 progress.FileSize.ToByteSize(),
909 var info = notification as StatusNotification;
912 StatusMessage = String.Format("Pithos {0}\r\n{1}",
913 _fileVersion.Value.FileVersion,
917 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
920 ShowBalloonFor(notification);
923 private void ShowBalloonFor(Notification notification)
925 Contract.Requires(notification!=null);
927 if (!Settings.ShowDesktopNotifications)
931 switch (notification.Level)
933 case TraceLevel.Info:
934 case TraceLevel.Verbose:
936 case TraceLevel.Error:
937 icon = BalloonIcon.Error;
939 case TraceLevel.Warning:
940 icon = BalloonIcon.Warning;
946 var tv = (ShellView) GetView();
947 System.Action clickAction = null;
948 if (notification is ExpirationNotification)
950 clickAction = () => ShowPreferences("AccountTab");
952 var balloon = new PithosBalloon
954 Title = notification.Title,
955 Message = notification.Message,
957 ClickAction = clickAction
959 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
964 public void Handle(ShowFilePropertiesEvent message)
967 throw new ArgumentNullException("message");
968 if (String.IsNullOrWhiteSpace(message.FileName) )
969 throw new ArgumentException("message");
970 Contract.EndContractBlock();
972 var fileName = message.FileName;
973 //TODO: Display file properties for non-container folders
974 if (File.Exists(fileName))
975 //Retrieve the full name with exact casing. Pithos names are case sensitive
976 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
977 else if (Directory.Exists(fileName))
978 //Retrieve the full name with exact casing. Pithos names are case sensitive
980 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
981 if (IsContainer(path))
982 ShowContainerProperties(path);
984 ShowFileProperties(path);
988 private bool IsContainer(string path)
990 var matchingFolders = from account in _accounts
991 from rootFolder in Directory.GetDirectories(account.AccountPath)
992 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
994 return matchingFolders.Any();
997 public FileStatus GetFileStatus(string localFileName)
999 if (String.IsNullOrWhiteSpace(localFileName))
1000 throw new ArgumentNullException("localFileName");
1001 Contract.EndContractBlock();
1003 var statusKeeper = IoC.Get<IStatusKeeper>();
1004 var status=statusKeeper.GetFileStatus(localFileName);
1008 public void RemoveAccountFromDatabase(AccountSettings account)
1010 var statusKeeper = IoC.Get<IStatusKeeper>();
1011 statusKeeper.ClearFolderStatus(account.RootPath);