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)), Export(typeof(ShellViewModel))]
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<Uri, PithosMonitor> _monitors = new ConcurrentDictionary<Uri, 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<Uri, 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 private readonly PollAgent _pollAgent;
128 private MiniStatusViewModel _miniStatus;
131 public MiniStatusViewModel MiniStatus
133 get { return _miniStatus; }
137 _miniStatus.Shell = this;
138 _miniStatus.Deactivated += (sender, arg) =>
140 _statusVisible = false;
141 NotifyOfPropertyChange(()=>MiniStatusCaption);
147 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
150 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
152 [ImportingConstructor]
153 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent)
158 _windowManager = windowManager;
159 //CHECK: Caliburn doesn't need explicit command construction
160 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
161 _statusChecker = statusChecker;
164 _events.Subscribe(this);
166 _pollAgent = pollAgent;
169 Proxy.SetFromSettings(settings);
171 StatusMessage = Settings.Accounts.Count==0
172 ? "No Accounts added\r\nPlease add an account"
175 _accounts.CollectionChanged += (sender, e) =>
177 NotifyOfPropertyChange(() => OpenFolderCaption);
178 NotifyOfPropertyChange(() => HasAccounts);
183 catch (Exception exc)
185 Log.Error("Error while starting the ShellViewModel",exc);
191 private void SetVersionMessage()
193 Assembly assembly = Assembly.GetExecutingAssembly();
194 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
195 VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);
198 public void OnStatusAction()
200 if (Accounts.Count==0)
202 ShowPreferences("AccountTab");
205 protected override void OnActivate()
211 //Must delay opening the upgrade window
212 //to avoid Windows Messages sent by the TaskbarIcon
213 TaskEx.Delay(5000).ContinueWith(_=>
214 Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
221 private void OnCheckFinished(object sender, bool updaterequired)
224 Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);
225 if (_manualUpgradeCheck)
227 _manualUpgradeCheck = false;
229 //Sparkle raises events on a background thread
230 Execute.OnUIThread(()=>
231 ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"}));
235 private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
237 Log.InfoFormat("Update detected {0}",e.LatestVersion);
240 public void CheckForUpgrade()
242 ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."});
244 _sparkle.updateDetected -= OnUpgradeDetected;
245 _sparkle.checkLoopFinished -= OnCheckFinished;
248 _manualUpgradeCheck = true;
250 _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
253 private void InitializeSparkle()
255 _sparkle = new Sparkle(Settings.UpdateUrl);
256 _sparkle.updateDetected += OnUpgradeDetected;
257 _sparkle.checkLoopFinished += OnCheckFinished;
258 _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
261 private async void StartMonitoring()
265 if (Settings.IgnoreCertificateErrors)
267 ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
270 var accounts = Settings.Accounts.Select(MonitorAccount);
271 await TaskEx.WhenAll(accounts);
272 _statusService = StatusService.Start();
275 catch (AggregateException exc)
279 Log.Error("Error while starting monitoring", e);
286 protected override void OnDeactivate(bool close)
288 base.OnDeactivate(close);
291 StatusService.Stop(_statusService);
292 _statusService = null;
296 public Task MonitorAccount(AccountSettings account)
298 return Task.Factory.StartNew(() =>
300 PithosMonitor monitor;
301 var accountName = account.AccountName;
303 if (Monitors.TryGetValue(account.AccountKey, out monitor))
305 //If the account is active
306 if (account.IsActive)
308 //The Api Key may have changed throuth the Preferences dialog
309 monitor.ApiKey = account.ApiKey;
310 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
311 monitor.RootPath = account.RootPath;
312 //Start the monitor. It's OK to start an already started monitor,
313 //it will just ignore the call
314 StartMonitor(monitor).Wait();
318 //If the account is inactive
319 //Stop and remove the monitor
320 RemoveMonitor(account.ServerUrl,accountName);
326 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
327 monitor = new PithosMonitor
329 UserName = accountName,
330 ApiKey = account.ApiKey,
331 StatusNotification = this,
332 RootPath = account.RootPath
334 //PithosMonitor uses MEF so we need to resolve it
335 IoC.BuildUp(monitor);
337 monitor.AuthenticationUrl = account.ServerUrl;
339 Monitors[account.AccountKey] = monitor;
341 if (account.IsActive)
343 //Don't start a monitor if it doesn't have an account and ApiKey
344 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
345 String.IsNullOrWhiteSpace(monitor.ApiKey))
347 StartMonitor(monitor);
353 protected override void OnViewLoaded(object view)
356 var window = (Window)view;
357 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
358 base.OnViewLoaded(view);
362 #region Status Properties
364 private string _statusMessage;
365 public string StatusMessage
367 get { return _statusMessage; }
370 _statusMessage = value;
371 NotifyOfPropertyChange(() => StatusMessage);
372 NotifyOfPropertyChange(() => TooltipMessage);
376 public string VersionMessage { get; set; }
378 private string _tooltipMessage;
379 public string TooltipMessage
383 return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
387 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
388 public ObservableConcurrentCollection<AccountInfo> Accounts
390 get { return _accounts; }
393 public bool HasAccounts
395 get { return _accounts.Count > 0; }
399 public string OpenFolderCaption
403 return (_accounts.Count == 0)
404 ? "No Accounts Defined"
405 : "Open Pithos Folder";
409 private string _pauseSyncCaption="Pause Synching";
410 public string PauseSyncCaption
412 get { return _pauseSyncCaption; }
415 _pauseSyncCaption = value;
416 NotifyOfPropertyChange(() => PauseSyncCaption);
420 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
421 public ObservableConcurrentCollection<FileEntry> RecentFiles
423 get { return _recentFiles; }
427 private string _statusIcon="../Images/Pithos.ico";
428 public string StatusIcon
430 get { return _statusIcon; }
433 //TODO: Ensure all status icons use the Pithos logo
435 NotifyOfPropertyChange(() => StatusIcon);
443 public void ShowPreferences()
445 ShowPreferences(null);
448 public void ShowPreferences(string currentTab)
451 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
452 _windowManager.ShowDialog(preferences);
456 public void AboutPithos()
458 var about = IoC.Get<AboutViewModel>();
459 about.LatestVersion=_sparkle.LatestVersion;
460 _windowManager.ShowWindow(about);
463 public void SendFeedback()
465 var feedBack = IoC.Get<FeedbackViewModel>();
466 _windowManager.ShowWindow(feedBack);
469 //public PithosCommand OpenPithosFolderCommand { get; private set; }
471 public void OpenPithosFolder()
473 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
476 Process.Start(account.RootPath);
479 public void OpenPithosFolder(AccountInfo account)
481 Process.Start(account.AccountPath);
486 public void GoToSite()
488 var site = Properties.Settings.Default.ProductionServer;
493 public void GoToSite(AccountInfo account)
495 var uri = account.SiteUri.Replace("http://","https://");
499 private bool _statusVisible;
501 public string MiniStatusCaption
505 return _statusVisible ? "Hide Status Window" : "Show Status Window";
509 public void ShowMiniStatus()
512 _windowManager.ShowWindow(MiniStatus);
515 if (MiniStatus.IsActive)
516 MiniStatus.TryClose();
518 _statusVisible=!_statusVisible;
520 NotifyOfPropertyChange(()=>MiniStatusCaption);
524 /// Open an explorer window to the target path's directory
525 /// and select the file
527 /// <param name="entry"></param>
528 public void GoToFile(FileEntry entry)
530 var fullPath = entry.FullPath;
531 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
533 Process.Start("explorer.exe","/select, " + fullPath);
536 public void OpenLogPath()
538 var pithosDataPath = PithosSettings.PithosDataPath;
540 Process.Start(pithosDataPath);
543 public void ShowFileProperties()
545 var account = Settings.Accounts.First(acc => acc.IsActive);
546 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
547 var files=dir.GetFiles();
549 var idx=r.Next(0, files.Length);
550 ShowFileProperties(files[idx].FullName);
553 public void ShowFileProperties(string filePath)
555 if (String.IsNullOrWhiteSpace(filePath))
556 throw new ArgumentNullException("filePath");
557 if (!File.Exists(filePath) && !Directory.Exists(filePath))
558 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
559 Contract.EndContractBlock();
561 var pair=(from monitor in Monitors
562 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
563 select monitor).FirstOrDefault();
564 var accountMonitor = pair.Value;
566 if (accountMonitor == null)
569 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
573 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
574 _windowManager.ShowWindow(fileProperties);
577 public void ShowContainerProperties()
579 var account = Settings.Accounts.First(acc => acc.IsActive);
580 var dir = new DirectoryInfo(account.RootPath);
581 var fullName = (from folder in dir.EnumerateDirectories()
582 where (folder.Attributes & FileAttributes.Hidden) == 0
583 select folder.FullName).First();
584 ShowContainerProperties(fullName);
587 public void ShowContainerProperties(string filePath)
589 if (String.IsNullOrWhiteSpace(filePath))
590 throw new ArgumentNullException("filePath");
591 if (!Directory.Exists(filePath))
592 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
593 Contract.EndContractBlock();
595 var pair=(from monitor in Monitors
596 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
597 select monitor).FirstOrDefault();
598 var accountMonitor = pair.Value;
599 var info = accountMonitor.GetContainerInfo(filePath);
603 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
604 _windowManager.ShowWindow(containerProperties);
607 public void SynchNow()
609 _pollAgent.SynchNow();
612 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
614 if (currentInfo==null)
615 throw new ArgumentNullException("currentInfo");
616 Contract.EndContractBlock();
617 var monitor = Monitors[currentInfo.AccountKey];
618 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
622 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
624 if (container == null)
625 throw new ArgumentNullException("container");
626 Contract.EndContractBlock();
628 var monitor = Monitors[container.AccountKey];
629 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
634 public void ToggleSynching()
637 foreach (var pair in Monitors)
639 var monitor = pair.Value;
640 monitor.Pause = !monitor.Pause;
641 isPaused = monitor.Pause;
645 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
646 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
647 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
650 public void ExitPithos()
652 foreach (var pair in Monitors)
654 var monitor = pair.Value;
658 ((Window)GetView()).Close();
663 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
665 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
666 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
667 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
668 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
669 }.ToDictionary(s => s.Status);
671 readonly IWindowManager _windowManager;
673 //private int _syncCount=0;
676 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
678 public void SetPithosStatus(PithosStatus status)
680 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
682 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
684 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
685 _pithosStatus = PithosStatus.InSynch;
687 _pithosStatus = status;
691 public void SetPithosStatus(PithosStatus status,string message)
693 StatusMessage = message;
694 SetPithosStatus(status);
700 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
702 public void UpdateStatus()
705 if (_iconNames.ContainsKey(_pithosStatus))
707 var info = _iconNames[_pithosStatus];
708 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
711 if (_pithosStatus == PithosStatus.InSynch)
712 StatusMessage = "All files up to date";
717 private Task StartMonitor(PithosMonitor monitor,int retries=0)
719 return Task.Factory.StartNew(() =>
721 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
725 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
729 catch (WebException exc)
731 if (AbandonRetry(monitor, retries))
734 HttpStatusCode statusCode =HttpStatusCode.OK;
735 var response = exc.Response as HttpWebResponse;
737 statusCode = response.StatusCode;
741 case HttpStatusCode.Unauthorized:
742 var message = String.Format("API Key Expired for {0}. Starting Renewal",
744 Log.Error(message, exc);
745 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
746 account.IsExpired = true;
747 Notify(new ExpirationNotification(account));
748 //TryAuthorize(monitor.UserName, retries).Wait();
750 case HttpStatusCode.ProxyAuthenticationRequired:
751 TryAuthenticateProxy(monitor,retries);
754 TryLater(monitor, exc, retries);
758 catch (Exception exc)
760 if (AbandonRetry(monitor, retries))
763 TryLater(monitor,exc,retries);
769 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
771 Execute.OnUIThread(() =>
773 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
774 proxyAccount.Settings = Settings;
775 if (true != _windowManager.ShowDialog(proxyAccount))
777 StartMonitor(monitor, retries);
778 NotifyOfPropertyChange(() => Accounts);
782 private bool AbandonRetry(PithosMonitor monitor, int retries)
786 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
788 _events.Publish(new Notification
789 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
796 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
798 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
799 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
800 _events.Publish(new Notification
801 {Title = "Error", Message = message, Level = TraceLevel.Error});
802 Log.Error(message, exc);
806 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
808 StatusMessage = status;
810 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
813 public void NotifyChangedFile(string filePath)
815 if (RecentFiles.Any(e => e.FullPath == filePath))
818 IProducerConsumerCollection<FileEntry> files=RecentFiles;
820 while (files.Count > 5)
821 files.TryTake(out popped);
822 var entry = new FileEntry { FullPath = filePath };
826 public void NotifyAccount(AccountInfo account)
830 //TODO: What happens to an existing account whose Token has changed?
831 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
832 account.SiteUri, Uri.EscapeDataString(account.Token),
833 Uri.EscapeDataString(account.UserName));
835 if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
836 Accounts.TryAdd(account);
840 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
842 if (conflictFiles == null)
844 //Convert to list to avoid multiple iterations
845 var files = conflictFiles.ToList();
850 //TODO: Create a more specific message. For now, just show a warning
851 NotifyForFiles(files,message,TraceLevel.Warning);
855 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
862 StatusMessage = message;
864 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
867 public void Notify(Notification notification)
869 _events.Publish(notification);
873 public void RemoveMonitor(string serverUrl,string accountName)
875 if (String.IsNullOrWhiteSpace(accountName))
878 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
879 if (accountInfo != null)
881 _accounts.TryRemove(accountInfo);
882 _pollAgent.RemoveAccount(accountInfo);
885 var accountKey = new Uri(new Uri(serverUrl),accountName);
886 PithosMonitor monitor;
887 if (Monitors.TryRemove(accountKey, out monitor))
890 //TODO: Also remove any pending actions for this account
891 //from the network queue
895 public void RefreshOverlays()
897 foreach (var pair in Monitors)
899 var monitor = pair.Value;
901 var path = monitor.RootPath;
903 if (String.IsNullOrWhiteSpace(path))
906 if (!Directory.Exists(path) && !File.Exists(path))
909 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
913 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
914 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
915 pathPointer, IntPtr.Zero);
919 Marshal.FreeHGlobal(pathPointer);
924 #region Event Handlers
926 public void Handle(SelectiveSynchChanges message)
928 PithosMonitor monitor;
929 if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
931 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
938 private bool _pollStarted;
939 private Sparkle _sparkle;
940 private bool _manualUpgradeCheck;
942 //SMELL: Doing so much work for notifications in the shell is wrong
943 //The notifications should be moved to their own view/viewmodel pair
944 //and different templates should be used for different message types
945 //This will also allow the addition of extra functionality, eg. actions
947 public void Handle(Notification notification)
951 if (!Settings.ShowDesktopNotifications)
954 if (notification is PollNotification)
959 if (notification is CloudNotification)
964 notification.Title = "Pithos+";
965 notification.Message = "Start Synchronisation";
968 var deleteNotification = notification as CloudDeleteNotification;
969 if (deleteNotification != null)
971 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
975 var progress = notification as ProgressNotification;
978 if (progress != null)
980 StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
982 progress.Block/(double)progress.TotalBlocks,
983 progress.FileSize.ToByteSize(),
988 var info = notification as StatusNotification;
991 StatusMessage = info.Title;
994 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
997 if (notification.Level <= TraceLevel.Warning)
998 ShowBalloonFor(notification);
1001 private void ShowBalloonFor(Notification notification)
1003 Contract.Requires(notification!=null);
1005 if (!Settings.ShowDesktopNotifications)
1009 switch (notification.Level)
1011 case TraceLevel.Verbose:
1013 case TraceLevel.Info:
1014 icon = BalloonIcon.Info;
1016 case TraceLevel.Error:
1017 icon = BalloonIcon.Error;
1019 case TraceLevel.Warning:
1020 icon = BalloonIcon.Warning;
1026 var tv = (ShellView) GetView();
1027 System.Action clickAction = null;
1028 if (notification is ExpirationNotification)
1030 clickAction = () => ShowPreferences("AccountTab");
1032 var balloon = new PithosBalloon
1034 Title = notification.Title,
1035 Message = notification.Message,
1037 ClickAction = clickAction
1039 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1044 public void Handle(ShowFilePropertiesEvent message)
1046 if (message == null)
1047 throw new ArgumentNullException("message");
1048 if (String.IsNullOrWhiteSpace(message.FileName) )
1049 throw new ArgumentException("message");
1050 Contract.EndContractBlock();
1052 var fileName = message.FileName;
1053 //TODO: Display file properties for non-container folders
1054 if (File.Exists(fileName))
1055 //Retrieve the full name with exact casing. Pithos names are case sensitive
1056 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1057 else if (Directory.Exists(fileName))
1058 //Retrieve the full name with exact casing. Pithos names are case sensitive
1060 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1061 if (IsContainer(path))
1062 ShowContainerProperties(path);
1064 ShowFileProperties(path);
1068 private bool IsContainer(string path)
1070 var matchingFolders = from account in _accounts
1071 from rootFolder in Directory.GetDirectories(account.AccountPath)
1072 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1074 return matchingFolders.Any();
1077 public FileStatus GetFileStatus(string localFileName)
1079 if (String.IsNullOrWhiteSpace(localFileName))
1080 throw new ArgumentNullException("localFileName");
1081 Contract.EndContractBlock();
1083 var statusKeeper = IoC.Get<IStatusKeeper>();
1084 var status=statusKeeper.GetFileStatus(localFileName);
1088 public void RemoveAccountFromDatabase(AccountSettings account)
1090 var statusKeeper = IoC.Get<IStatusKeeper>();
1091 statusKeeper.ClearFolderStatus(account.RootPath);