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<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 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 var accounts = Settings.Accounts.Select(MonitorAccount);
266 await TaskEx.WhenAll(accounts);
267 _statusService = StatusService.Start();
270 foreach (var account in Settings.Accounts)
272 await MonitorAccount(account);
277 catch (AggregateException exc)
281 Log.Error("Error while starting monitoring", e);
288 protected override void OnDeactivate(bool close)
290 base.OnDeactivate(close);
293 StatusService.Stop(_statusService);
294 _statusService = null;
298 public Task MonitorAccount(AccountSettings account)
300 return Task.Factory.StartNew(() =>
302 PithosMonitor monitor;
303 var accountName = account.AccountName;
305 if (_monitors.TryGetValue(accountName, out monitor))
307 //If the account is active
308 if (account.IsActive)
310 //The Api Key may have changed throuth the Preferences dialog
311 monitor.ApiKey = account.ApiKey;
312 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
313 monitor.RootPath = account.RootPath;
314 //Start the monitor. It's OK to start an already started monitor,
315 //it will just ignore the call
316 StartMonitor(monitor).Wait();
320 //If the account is inactive
321 //Stop and remove the monitor
322 RemoveMonitor(accountName);
328 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
329 monitor = new PithosMonitor
331 UserName = accountName,
332 ApiKey = account.ApiKey,
333 StatusNotification = this,
334 RootPath = account.RootPath
336 //PithosMonitor uses MEF so we need to resolve it
337 IoC.BuildUp(monitor);
339 monitor.AuthenticationUrl = account.ServerUrl;
341 _monitors[accountName] = monitor;
343 if (account.IsActive)
345 //Don't start a monitor if it doesn't have an account and ApiKey
346 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
347 String.IsNullOrWhiteSpace(monitor.ApiKey))
349 StartMonitor(monitor);
355 protected override void OnViewLoaded(object view)
358 var window = (Window)view;
359 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
360 base.OnViewLoaded(view);
364 #region Status Properties
366 private string _statusMessage;
367 public string StatusMessage
369 get { return _statusMessage; }
372 _statusMessage = value;
373 NotifyOfPropertyChange(() => StatusMessage);
374 NotifyOfPropertyChange(() => TooltipMessage);
378 public string VersionMessage { get; set; }
380 private string _tooltipMessage;
381 public string TooltipMessage
385 return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
389 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
390 public ObservableConcurrentCollection<AccountInfo> Accounts
392 get { return _accounts; }
395 public bool HasAccounts
397 get { return _accounts.Count > 0; }
401 public string OpenFolderCaption
405 return (_accounts.Count == 0)
406 ? "No Accounts Defined"
407 : "Open Pithos Folder";
411 private string _pauseSyncCaption="Pause Synching";
412 public string PauseSyncCaption
414 get { return _pauseSyncCaption; }
417 _pauseSyncCaption = value;
418 NotifyOfPropertyChange(() => PauseSyncCaption);
422 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
423 public ObservableConcurrentCollection<FileEntry> RecentFiles
425 get { return _recentFiles; }
429 private string _statusIcon="../Images/Pithos.ico";
430 public string StatusIcon
432 get { return _statusIcon; }
435 //TODO: Ensure all status icons use the Pithos logo
437 NotifyOfPropertyChange(() => StatusIcon);
445 public void ShowPreferences()
447 ShowPreferences(null);
450 public void ShowPreferences(string currentTab)
453 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
454 _windowManager.ShowDialog(preferences);
458 public void AboutPithos()
460 var about = IoC.Get<AboutViewModel>();
461 about.LatestVersion=_sparkle.LatestVersion;
462 _windowManager.ShowWindow(about);
465 public void SendFeedback()
467 var feedBack = IoC.Get<FeedbackViewModel>();
468 _windowManager.ShowWindow(feedBack);
471 //public PithosCommand OpenPithosFolderCommand { get; private set; }
473 public void OpenPithosFolder()
475 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
478 Process.Start(account.RootPath);
481 public void OpenPithosFolder(AccountInfo account)
483 Process.Start(account.AccountPath);
488 public void GoToSite()
490 var site = Properties.Settings.Default.ProductionServer;
495 public void GoToSite(AccountInfo account)
497 var uri = account.SiteUri.Replace("http://","https://");
501 private bool _statusVisible;
503 public string MiniStatusCaption
507 return _statusVisible ? "Hide Status Window" : "Show Status Window";
511 public void ShowMiniStatus()
514 _windowManager.ShowWindow(MiniStatus);
517 if (MiniStatus.IsActive)
518 MiniStatus.TryClose();
520 _statusVisible=!_statusVisible;
522 NotifyOfPropertyChange(()=>MiniStatusCaption);
526 /// Open an explorer window to the target path's directory
527 /// and select the file
529 /// <param name="entry"></param>
530 public void GoToFile(FileEntry entry)
532 var fullPath = entry.FullPath;
533 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
535 Process.Start("explorer.exe","/select, " + fullPath);
538 public void OpenLogPath()
540 var pithosDataPath = PithosSettings.PithosDataPath;
542 Process.Start(pithosDataPath);
545 public void ShowFileProperties()
547 var account = Settings.Accounts.First(acc => acc.IsActive);
548 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
549 var files=dir.GetFiles();
551 var idx=r.Next(0, files.Length);
552 ShowFileProperties(files[idx].FullName);
555 public void ShowFileProperties(string filePath)
557 if (String.IsNullOrWhiteSpace(filePath))
558 throw new ArgumentNullException("filePath");
559 if (!File.Exists(filePath) && !Directory.Exists(filePath))
560 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
561 Contract.EndContractBlock();
563 var pair=(from monitor in Monitors
564 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
565 select monitor).FirstOrDefault();
566 var accountMonitor = pair.Value;
568 if (accountMonitor == null)
571 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
575 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
576 _windowManager.ShowWindow(fileProperties);
579 public void ShowContainerProperties()
581 var account = Settings.Accounts.First(acc => acc.IsActive);
582 var dir = new DirectoryInfo(account.RootPath);
583 var fullName = (from folder in dir.EnumerateDirectories()
584 where (folder.Attributes & FileAttributes.Hidden) == 0
585 select folder.FullName).First();
586 ShowContainerProperties(fullName);
589 public void ShowContainerProperties(string filePath)
591 if (String.IsNullOrWhiteSpace(filePath))
592 throw new ArgumentNullException("filePath");
593 if (!Directory.Exists(filePath))
594 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
595 Contract.EndContractBlock();
597 var pair=(from monitor in Monitors
598 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
599 select monitor).FirstOrDefault();
600 var accountMonitor = pair.Value;
601 var info = accountMonitor.GetContainerInfo(filePath);
605 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
606 _windowManager.ShowWindow(containerProperties);
609 public void SynchNow()
611 _pollAgent.SynchNow();
614 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
616 if (currentInfo==null)
617 throw new ArgumentNullException("currentInfo");
618 Contract.EndContractBlock();
620 var monitor = Monitors[currentInfo.Account];
621 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
625 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
627 if (container == null)
628 throw new ArgumentNullException("container");
629 Contract.EndContractBlock();
631 var monitor = Monitors[container.Account];
632 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
637 public void ToggleSynching()
640 foreach (var pair in Monitors)
642 var monitor = pair.Value;
643 monitor.Pause = !monitor.Pause;
644 isPaused = monitor.Pause;
648 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
649 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
650 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
653 public void ExitPithos()
655 foreach (var pair in Monitors)
657 var monitor = pair.Value;
661 ((Window)GetView()).Close();
666 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
668 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
669 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
670 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
671 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
672 }.ToDictionary(s => s.Status);
674 readonly IWindowManager _windowManager;
676 //private int _syncCount=0;
679 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
681 public void SetPithosStatus(PithosStatus status)
683 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
685 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
687 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
688 _pithosStatus = PithosStatus.InSynch;
690 _pithosStatus = status;
694 public void SetPithosStatus(PithosStatus status,string message)
696 StatusMessage = message;
697 SetPithosStatus(status);
703 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
705 public void UpdateStatus()
708 if (_iconNames.ContainsKey(_pithosStatus))
710 var info = _iconNames[_pithosStatus];
711 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
714 if (_pithosStatus == PithosStatus.InSynch)
715 StatusMessage = "All files up to date";
720 private Task StartMonitor(PithosMonitor monitor,int retries=0)
722 return Task.Factory.StartNew(() =>
724 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
728 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
732 catch (WebException exc)
734 if (AbandonRetry(monitor, retries))
737 HttpStatusCode statusCode =HttpStatusCode.OK;
738 var response = exc.Response as HttpWebResponse;
740 statusCode = response.StatusCode;
744 case HttpStatusCode.Unauthorized:
745 var message = String.Format("API Key Expired for {0}. Starting Renewal",
747 Log.Error(message, exc);
748 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
749 account.IsExpired = true;
750 Notify(new ExpirationNotification(account));
751 //TryAuthorize(monitor.UserName, retries).Wait();
753 case HttpStatusCode.ProxyAuthenticationRequired:
754 TryAuthenticateProxy(monitor,retries);
757 TryLater(monitor, exc, retries);
761 catch (Exception exc)
763 if (AbandonRetry(monitor, retries))
766 TryLater(monitor,exc,retries);
772 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
774 Execute.OnUIThread(() =>
776 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
777 proxyAccount.Settings = Settings;
778 if (true != _windowManager.ShowDialog(proxyAccount))
780 StartMonitor(monitor, retries);
781 NotifyOfPropertyChange(() => Accounts);
785 private bool AbandonRetry(PithosMonitor monitor, int retries)
789 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
791 _events.Publish(new Notification
792 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
799 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
801 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
802 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
803 _events.Publish(new Notification
804 {Title = "Error", Message = message, Level = TraceLevel.Error});
805 Log.Error(message, exc);
809 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
811 StatusMessage = status;
813 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
816 public void NotifyChangedFile(string filePath)
818 if (RecentFiles.Any(e => e.FullPath == filePath))
821 IProducerConsumerCollection<FileEntry> files=RecentFiles;
823 while (files.Count > 5)
824 files.TryTake(out popped);
825 var entry = new FileEntry { FullPath = filePath };
829 public void NotifyAccount(AccountInfo account)
833 //TODO: What happens to an existing account whose Token has changed?
834 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
835 account.SiteUri, Uri.EscapeDataString(account.Token),
836 Uri.EscapeDataString(account.UserName));
838 if (Accounts.All(item => item.UserName != account.UserName))
839 Accounts.TryAdd(account);
843 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
845 if (conflictFiles == null)
847 //Convert to list to avoid multiple iterations
848 var files = conflictFiles.ToList();
853 //TODO: Create a more specific message. For now, just show a warning
854 NotifyForFiles(files,message,TraceLevel.Warning);
858 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
865 StatusMessage = message;
867 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
870 public void Notify(Notification notification)
872 _events.Publish(notification);
876 public void RemoveMonitor(string accountName)
878 if (String.IsNullOrWhiteSpace(accountName))
881 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
882 if (accountInfo != null)
884 _accounts.TryRemove(accountInfo);
885 _pollAgent.RemoveAccount(accountInfo);
888 PithosMonitor monitor;
889 if (Monitors.TryRemove(accountName, out monitor))
892 //TODO: Also remove any pending actions for this account
893 //from the network queue
897 public void RefreshOverlays()
899 foreach (var pair in Monitors)
901 var monitor = pair.Value;
903 var path = monitor.RootPath;
905 if (String.IsNullOrWhiteSpace(path))
908 if (!Directory.Exists(path) && !File.Exists(path))
911 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
915 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
916 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
917 pathPointer, IntPtr.Zero);
921 Marshal.FreeHGlobal(pathPointer);
926 #region Event Handlers
928 public void Handle(SelectiveSynchChanges message)
930 var accountName = message.Account.AccountName;
931 PithosMonitor monitor;
932 if (_monitors.TryGetValue(accountName, out monitor))
934 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
941 private bool _pollStarted;
942 private Sparkle _sparkle;
943 private bool _manualUpgradeCheck;
945 //SMELL: Doing so much work for notifications in the shell is wrong
946 //The notifications should be moved to their own view/viewmodel pair
947 //and different templates should be used for different message types
948 //This will also allow the addition of extra functionality, eg. actions
950 public void Handle(Notification notification)
954 if (!Settings.ShowDesktopNotifications)
957 if (notification is PollNotification)
962 if (notification is CloudNotification)
967 notification.Title = "Pithos+";
968 notification.Message = "Start Synchronisation";
971 var deleteNotification = notification as CloudDeleteNotification;
972 if (deleteNotification != null)
974 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
978 var progress = notification as ProgressNotification;
981 if (progress != null)
983 StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
985 progress.Block/(double)progress.TotalBlocks,
986 progress.FileSize.ToByteSize(),
991 var info = notification as StatusNotification;
994 StatusMessage = info.Title;
997 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
1000 if (notification.Level <= TraceLevel.Warning)
1001 ShowBalloonFor(notification);
1004 private void ShowBalloonFor(Notification notification)
1006 Contract.Requires(notification!=null);
1008 if (!Settings.ShowDesktopNotifications)
1012 switch (notification.Level)
1014 case TraceLevel.Verbose:
1016 case TraceLevel.Info:
1017 icon = BalloonIcon.Info;
1019 case TraceLevel.Error:
1020 icon = BalloonIcon.Error;
1022 case TraceLevel.Warning:
1023 icon = BalloonIcon.Warning;
1029 var tv = (ShellView) GetView();
1030 System.Action clickAction = null;
1031 if (notification is ExpirationNotification)
1033 clickAction = () => ShowPreferences("AccountTab");
1035 var balloon = new PithosBalloon
1037 Title = notification.Title,
1038 Message = notification.Message,
1040 ClickAction = clickAction
1042 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1047 public void Handle(ShowFilePropertiesEvent message)
1049 if (message == null)
1050 throw new ArgumentNullException("message");
1051 if (String.IsNullOrWhiteSpace(message.FileName) )
1052 throw new ArgumentException("message");
1053 Contract.EndContractBlock();
1055 var fileName = message.FileName;
1056 //TODO: Display file properties for non-container folders
1057 if (File.Exists(fileName))
1058 //Retrieve the full name with exact casing. Pithos names are case sensitive
1059 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1060 else if (Directory.Exists(fileName))
1061 //Retrieve the full name with exact casing. Pithos names are case sensitive
1063 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1064 if (IsContainer(path))
1065 ShowContainerProperties(path);
1067 ShowFileProperties(path);
1071 private bool IsContainer(string path)
1073 var matchingFolders = from account in _accounts
1074 from rootFolder in Directory.GetDirectories(account.AccountPath)
1075 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1077 return matchingFolders.Any();
1080 public FileStatus GetFileStatus(string localFileName)
1082 if (String.IsNullOrWhiteSpace(localFileName))
1083 throw new ArgumentNullException("localFileName");
1084 Contract.EndContractBlock();
1086 var statusKeeper = IoC.Get<IStatusKeeper>();
1087 var status=statusKeeper.GetFileStatus(localFileName);
1091 public void RemoveAccountFromDatabase(AccountSettings account)
1093 var statusKeeper = IoC.Get<IStatusKeeper>();
1094 statusKeeper.ClearFolderStatus(account.RootPath);