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 public string TooltipMessage
382 return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
386 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
387 public ObservableConcurrentCollection<AccountInfo> Accounts
389 get { return _accounts; }
392 public bool HasAccounts
394 get { return _accounts.Count > 0; }
398 public string OpenFolderCaption
402 return (_accounts.Count == 0)
403 ? "No Accounts Defined"
404 : "Open Pithos Folder";
408 private string _pauseSyncCaption="Pause Synching";
409 public string PauseSyncCaption
411 get { return _pauseSyncCaption; }
414 _pauseSyncCaption = value;
415 NotifyOfPropertyChange(() => PauseSyncCaption);
419 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
420 public ObservableConcurrentCollection<FileEntry> RecentFiles
422 get { return _recentFiles; }
426 private string _statusIcon="../Images/Pithos.ico";
427 public string StatusIcon
429 get { return _statusIcon; }
432 //TODO: Ensure all status icons use the Pithos logo
434 NotifyOfPropertyChange(() => StatusIcon);
442 public void ShowPreferences()
444 ShowPreferences(null);
447 public void ShowPreferences(string currentTab)
450 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
451 _windowManager.ShowDialog(preferences);
455 public void AboutPithos()
457 var about = IoC.Get<AboutViewModel>();
458 about.LatestVersion=_sparkle.LatestVersion;
459 _windowManager.ShowWindow(about);
462 public void SendFeedback()
464 var feedBack = IoC.Get<FeedbackViewModel>();
465 _windowManager.ShowWindow(feedBack);
468 //public PithosCommand OpenPithosFolderCommand { get; private set; }
470 public void OpenPithosFolder()
472 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
475 Process.Start(account.RootPath);
478 public void OpenPithosFolder(AccountInfo account)
480 Process.Start(account.AccountPath);
485 public void GoToSite()
487 var site = Properties.Settings.Default.ProductionServer;
492 public void GoToSite(AccountInfo account)
494 var uri = account.SiteUri.Replace("http://","https://");
498 private bool _statusVisible;
500 public string MiniStatusCaption
504 return _statusVisible ? "Hide Status Window" : "Show Status Window";
508 public void ShowMiniStatus()
511 _windowManager.ShowWindow(MiniStatus);
514 if (MiniStatus.IsActive)
515 MiniStatus.TryClose();
517 _statusVisible=!_statusVisible;
519 NotifyOfPropertyChange(()=>MiniStatusCaption);
523 /// Open an explorer window to the target path's directory
524 /// and select the file
526 /// <param name="entry"></param>
527 public void GoToFile(FileEntry entry)
529 var fullPath = entry.FullPath;
530 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
532 Process.Start("explorer.exe","/select, " + fullPath);
535 public void OpenLogPath()
537 var pithosDataPath = PithosSettings.PithosDataPath;
539 Process.Start(pithosDataPath);
542 public void ShowFileProperties()
544 var account = Settings.Accounts.First(acc => acc.IsActive);
545 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
546 var files=dir.GetFiles();
548 var idx=r.Next(0, files.Length);
549 ShowFileProperties(files[idx].FullName);
552 public void ShowFileProperties(string filePath)
554 if (String.IsNullOrWhiteSpace(filePath))
555 throw new ArgumentNullException("filePath");
556 if (!File.Exists(filePath) && !Directory.Exists(filePath))
557 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
558 Contract.EndContractBlock();
560 var pair=(from monitor in Monitors
561 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
562 select monitor).FirstOrDefault();
563 var accountMonitor = pair.Value;
565 if (accountMonitor == null)
568 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
572 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
573 _windowManager.ShowWindow(fileProperties);
576 public void ShowContainerProperties()
578 var account = Settings.Accounts.First(acc => acc.IsActive);
579 var dir = new DirectoryInfo(account.RootPath);
580 var fullName = (from folder in dir.EnumerateDirectories()
581 where (folder.Attributes & FileAttributes.Hidden) == 0
582 select folder.FullName).First();
583 ShowContainerProperties(fullName);
586 public void ShowContainerProperties(string filePath)
588 if (String.IsNullOrWhiteSpace(filePath))
589 throw new ArgumentNullException("filePath");
590 if (!Directory.Exists(filePath))
591 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
592 Contract.EndContractBlock();
594 var pair=(from monitor in Monitors
595 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
596 select monitor).FirstOrDefault();
597 var accountMonitor = pair.Value;
598 var info = accountMonitor.GetContainerInfo(filePath);
602 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
603 _windowManager.ShowWindow(containerProperties);
606 public void SynchNow()
608 _pollAgent.SynchNow();
611 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
613 if (currentInfo==null)
614 throw new ArgumentNullException("currentInfo");
615 Contract.EndContractBlock();
616 var monitor = Monitors[currentInfo.AccountKey];
617 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
621 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
623 if (container == null)
624 throw new ArgumentNullException("container");
625 Contract.EndContractBlock();
627 var monitor = Monitors[container.AccountKey];
628 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
633 public void ToggleSynching()
636 foreach (var pair in Monitors)
638 var monitor = pair.Value;
639 monitor.Pause = !monitor.Pause;
640 isPaused = monitor.Pause;
644 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
645 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
646 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
649 public void ExitPithos()
654 foreach (var monitor in Monitors.Select(pair => pair.Value))
659 var view = GetView() as Window;
663 Application.Current.Shutdown();
665 catch (Exception exc)
667 Log.Info("Exception while exiting", exc);
668 Application.Current.Shutdown();
675 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
677 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
678 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
679 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
680 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
681 }.ToDictionary(s => s.Status);
683 readonly IWindowManager _windowManager;
685 //private int _syncCount=0;
688 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
690 public void SetPithosStatus(PithosStatus status)
692 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
694 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
696 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
697 _pithosStatus = PithosStatus.InSynch;
699 _pithosStatus = status;
703 public void SetPithosStatus(PithosStatus status,string message)
705 StatusMessage = message;
706 SetPithosStatus(status);
712 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
714 public void UpdateStatus()
717 if (_iconNames.ContainsKey(_pithosStatus))
719 var info = _iconNames[_pithosStatus];
720 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
723 if (_pithosStatus == PithosStatus.InSynch)
724 StatusMessage = "All files up to date";
729 private Task StartMonitor(PithosMonitor monitor,int retries=0)
731 return Task.Factory.StartNew(() =>
733 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
737 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
741 catch (WebException exc)
743 if (AbandonRetry(monitor, retries))
746 HttpStatusCode statusCode =HttpStatusCode.OK;
747 var response = exc.Response as HttpWebResponse;
749 statusCode = response.StatusCode;
753 case HttpStatusCode.Unauthorized:
754 var message = String.Format("API Key Expired for {0}. Starting Renewal",
756 Log.Error(message, exc);
757 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
758 account.IsExpired = true;
759 Notify(new ExpirationNotification(account));
760 //TryAuthorize(monitor.UserName, retries).Wait();
762 case HttpStatusCode.ProxyAuthenticationRequired:
763 TryAuthenticateProxy(monitor,retries);
766 TryLater(monitor, exc, retries);
770 catch (Exception exc)
772 if (AbandonRetry(monitor, retries))
775 TryLater(monitor,exc,retries);
781 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
783 Execute.OnUIThread(() =>
785 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
786 proxyAccount.Settings = Settings;
787 if (true != _windowManager.ShowDialog(proxyAccount))
789 StartMonitor(monitor, retries);
790 NotifyOfPropertyChange(() => Accounts);
794 private bool AbandonRetry(PithosMonitor monitor, int retries)
798 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
800 _events.Publish(new Notification
801 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
808 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
810 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
811 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
812 _events.Publish(new Notification
813 {Title = "Error", Message = message, Level = TraceLevel.Error});
814 Log.Error(message, exc);
818 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
820 StatusMessage = status;
822 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
825 public void NotifyChangedFile(string filePath)
827 if (RecentFiles.Any(e => e.FullPath == filePath))
830 IProducerConsumerCollection<FileEntry> files=RecentFiles;
832 while (files.Count > 5)
833 files.TryTake(out popped);
834 var entry = new FileEntry { FullPath = filePath };
838 public void NotifyAccount(AccountInfo account)
842 //TODO: What happens to an existing account whose Token has changed?
843 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
844 account.SiteUri, Uri.EscapeDataString(account.Token),
845 Uri.EscapeDataString(account.UserName));
847 if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
848 Accounts.TryAdd(account);
852 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
854 if (conflictFiles == null)
856 //Convert to list to avoid multiple iterations
857 var files = conflictFiles.ToList();
862 //TODO: Create a more specific message. For now, just show a warning
863 NotifyForFiles(files,message,TraceLevel.Warning);
867 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
874 StatusMessage = message;
876 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
879 public void Notify(Notification notification)
881 _events.Publish(notification);
885 public void RemoveMonitor(string serverUrl,string accountName)
887 if (String.IsNullOrWhiteSpace(accountName))
890 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
891 if (accountInfo != null)
893 _accounts.TryRemove(accountInfo);
894 _pollAgent.RemoveAccount(accountInfo);
897 var accountKey = new Uri(new Uri(serverUrl),accountName);
898 PithosMonitor monitor;
899 if (Monitors.TryRemove(accountKey, out monitor))
902 //TODO: Also remove any pending actions for this account
903 //from the network queue
907 public void RefreshOverlays()
909 foreach (var pair in Monitors)
911 var monitor = pair.Value;
913 var path = monitor.RootPath;
915 if (String.IsNullOrWhiteSpace(path))
918 if (!Directory.Exists(path) && !File.Exists(path))
921 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
925 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
926 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
927 pathPointer, IntPtr.Zero);
931 Marshal.FreeHGlobal(pathPointer);
936 #region Event Handlers
938 public void Handle(SelectiveSynchChanges message)
940 PithosMonitor monitor;
941 if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
943 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
950 private bool _pollStarted;
951 private Sparkle _sparkle;
952 private bool _manualUpgradeCheck;
954 //SMELL: Doing so much work for notifications in the shell is wrong
955 //The notifications should be moved to their own view/viewmodel pair
956 //and different templates should be used for different message types
957 //This will also allow the addition of extra functionality, eg. actions
959 public void Handle(Notification notification)
963 if (!Settings.ShowDesktopNotifications)
966 if (notification is PollNotification)
971 if (notification is CloudNotification)
976 notification.Title = "Pithos+";
977 notification.Message = "Start Synchronisation";
980 var deleteNotification = notification as CloudDeleteNotification;
981 if (deleteNotification != null)
983 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
987 var progress = notification as ProgressNotification;
990 if (progress != null)
992 StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
994 progress.Block/(double)progress.TotalBlocks,
995 progress.FileSize.ToByteSize(),
1000 var info = notification as StatusNotification;
1003 StatusMessage = info.Title;
1006 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
1009 if (notification.Level <= TraceLevel.Warning)
1010 ShowBalloonFor(notification);
1013 private void ShowBalloonFor(Notification notification)
1015 Contract.Requires(notification!=null);
1017 if (!Settings.ShowDesktopNotifications)
1021 switch (notification.Level)
1023 case TraceLevel.Verbose:
1025 case TraceLevel.Info:
1026 icon = BalloonIcon.Info;
1028 case TraceLevel.Error:
1029 icon = BalloonIcon.Error;
1031 case TraceLevel.Warning:
1032 icon = BalloonIcon.Warning;
1038 var tv = (ShellView) GetView();
1039 System.Action clickAction = null;
1040 if (notification is ExpirationNotification)
1042 clickAction = () => ShowPreferences("AccountTab");
1044 var balloon = new PithosBalloon
1046 Title = notification.Title,
1047 Message = notification.Message,
1049 ClickAction = clickAction
1051 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1056 public void Handle(ShowFilePropertiesEvent message)
1058 if (message == null)
1059 throw new ArgumentNullException("message");
1060 if (String.IsNullOrWhiteSpace(message.FileName) )
1061 throw new ArgumentException("message");
1062 Contract.EndContractBlock();
1064 var fileName = message.FileName;
1065 //TODO: Display file properties for non-container folders
1066 if (File.Exists(fileName))
1067 //Retrieve the full name with exact casing. Pithos names are case sensitive
1068 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1069 else if (Directory.Exists(fileName))
1070 //Retrieve the full name with exact casing. Pithos names are case sensitive
1072 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1073 if (IsContainer(path))
1074 ShowContainerProperties(path);
1076 ShowFileProperties(path);
1080 private bool IsContainer(string path)
1082 var matchingFolders = from account in _accounts
1083 from rootFolder in Directory.GetDirectories(account.AccountPath)
1084 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1086 return matchingFolders.Any();
1089 public FileStatus GetFileStatus(string localFileName)
1091 if (String.IsNullOrWhiteSpace(localFileName))
1092 throw new ArgumentNullException("localFileName");
1093 Contract.EndContractBlock();
1095 var statusKeeper = IoC.Get<IStatusKeeper>();
1096 var status=statusKeeper.GetFileStatus(localFileName);
1100 public void RemoveAccountFromDatabase(AccountSettings account)
1102 var statusKeeper = IoC.Get<IStatusKeeper>();
1103 statusKeeper.ClearFolderStatus(account.RootPath);