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 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 Pitsos+ 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."});
243 Log.Error("Test Error message");
245 _sparkle.updateDetected -= OnUpgradeDetected;
246 _sparkle.checkLoopFinished -= OnCheckFinished;
249 _manualUpgradeCheck = true;
251 _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
254 private void InitializeSparkle()
256 _sparkle = new Sparkle(Settings.UpdateUrl);
257 _sparkle.updateDetected += OnUpgradeDetected;
258 _sparkle.checkLoopFinished += OnCheckFinished;
259 _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
262 private async void StartMonitoring()
266 var accounts = Settings.Accounts.Select(MonitorAccount);
267 await TaskEx.WhenAll(accounts);
268 _statusService = StatusService.Start();
271 foreach (var account in Settings.Accounts)
273 await MonitorAccount(account);
278 catch (AggregateException exc)
282 Log.Error("Error while starting monitoring", e);
289 protected override void OnDeactivate(bool close)
291 base.OnDeactivate(close);
294 StatusService.Stop(_statusService);
295 _statusService = null;
299 public Task MonitorAccount(AccountSettings account)
301 return Task.Factory.StartNew(() =>
303 PithosMonitor monitor;
304 var accountName = account.AccountName;
306 if (_monitors.TryGetValue(accountName, out monitor))
308 //If the account is active
309 if (account.IsActive)
311 //The Api Key may have changed throuth the Preferences dialog
312 monitor.ApiKey = account.ApiKey;
313 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
314 monitor.RootPath = account.RootPath;
315 //Start the monitor. It's OK to start an already started monitor,
316 //it will just ignore the call
317 StartMonitor(monitor).Wait();
321 //If the account is inactive
322 //Stop and remove the monitor
323 RemoveMonitor(accountName);
329 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
330 monitor = new PithosMonitor
332 UserName = accountName,
333 ApiKey = account.ApiKey,
334 StatusNotification = this,
335 RootPath = account.RootPath
337 //PithosMonitor uses MEF so we need to resolve it
338 IoC.BuildUp(monitor);
340 monitor.AuthenticationUrl = account.ServerUrl;
342 _monitors[accountName] = monitor;
344 if (account.IsActive)
346 //Don't start a monitor if it doesn't have an account and ApiKey
347 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
348 String.IsNullOrWhiteSpace(monitor.ApiKey))
350 StartMonitor(monitor);
356 protected override void OnViewLoaded(object view)
359 var window = (Window)view;
360 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
361 base.OnViewLoaded(view);
365 #region Status Properties
367 private string _statusMessage;
368 public string StatusMessage
370 get { return _statusMessage; }
373 _statusMessage = value;
374 NotifyOfPropertyChange(() => StatusMessage);
375 NotifyOfPropertyChange(() => TooltipMessage);
379 public string VersionMessage { get; set; }
381 private string _tooltipMessage;
382 public string TooltipMessage
386 return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
390 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
391 public ObservableConcurrentCollection<AccountInfo> Accounts
393 get { return _accounts; }
396 public bool HasAccounts
398 get { return _accounts.Count > 0; }
402 public string OpenFolderCaption
406 return (_accounts.Count == 0)
407 ? "No Accounts Defined"
408 : "Open Pithos Folder";
412 private string _pauseSyncCaption="Pause Synching";
413 public string PauseSyncCaption
415 get { return _pauseSyncCaption; }
418 _pauseSyncCaption = value;
419 NotifyOfPropertyChange(() => PauseSyncCaption);
423 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
424 public ObservableConcurrentCollection<FileEntry> RecentFiles
426 get { return _recentFiles; }
430 private string _statusIcon="../Images/Pithos.ico";
431 public string StatusIcon
433 get { return _statusIcon; }
436 //TODO: Ensure all status icons use the Pithos logo
438 NotifyOfPropertyChange(() => StatusIcon);
446 public void ShowPreferences()
448 ShowPreferences(null);
451 public void ShowPreferences(string currentTab)
454 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
455 _windowManager.ShowDialog(preferences);
459 public void AboutPithos()
461 var about = IoC.Get<AboutViewModel>();
462 about.LatestVersion=_sparkle.LatestVersion;
463 _windowManager.ShowWindow(about);
466 public void SendFeedback()
468 var feedBack = IoC.Get<FeedbackViewModel>();
469 _windowManager.ShowWindow(feedBack);
472 //public PithosCommand OpenPithosFolderCommand { get; private set; }
474 public void OpenPithosFolder()
476 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
479 Process.Start(account.RootPath);
482 public void OpenPithosFolder(AccountInfo account)
484 Process.Start(account.AccountPath);
489 public void GoToSite()
491 var site = Properties.Settings.Default.PithosSite;
496 public void GoToSite(AccountInfo account)
498 var uri = account.SiteUri.Replace("http://","https://");
502 private bool _statusVisible;
504 public string MiniStatusCaption
508 return _statusVisible ? "Hide Status Window" : "Show Status Window";
512 public void ShowMiniStatus()
515 _windowManager.ShowWindow(MiniStatus);
518 if (MiniStatus.IsActive)
519 MiniStatus.TryClose();
521 _statusVisible=!_statusVisible;
523 NotifyOfPropertyChange(()=>MiniStatusCaption);
527 /// Open an explorer window to the target path's directory
528 /// and select the file
530 /// <param name="entry"></param>
531 public void GoToFile(FileEntry entry)
533 var fullPath = entry.FullPath;
534 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
536 Process.Start("explorer.exe","/select, " + fullPath);
539 public void OpenLogPath()
541 var pithosDataPath = PithosSettings.PithosDataPath;
543 Process.Start(pithosDataPath);
546 public void ShowFileProperties()
548 var account = Settings.Accounts.First(acc => acc.IsActive);
549 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
550 var files=dir.GetFiles();
552 var idx=r.Next(0, files.Length);
553 ShowFileProperties(files[idx].FullName);
556 public void ShowFileProperties(string filePath)
558 if (String.IsNullOrWhiteSpace(filePath))
559 throw new ArgumentNullException("filePath");
560 if (!File.Exists(filePath) && !Directory.Exists(filePath))
561 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
562 Contract.EndContractBlock();
564 var pair=(from monitor in Monitors
565 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
566 select monitor).FirstOrDefault();
567 var accountMonitor = pair.Value;
569 if (accountMonitor == null)
572 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
576 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
577 _windowManager.ShowWindow(fileProperties);
580 public void ShowContainerProperties()
582 var account = Settings.Accounts.First(acc => acc.IsActive);
583 var dir = new DirectoryInfo(account.RootPath);
584 var fullName = (from folder in dir.EnumerateDirectories()
585 where (folder.Attributes & FileAttributes.Hidden) == 0
586 select folder.FullName).First();
587 ShowContainerProperties(fullName);
590 public void ShowContainerProperties(string filePath)
592 if (String.IsNullOrWhiteSpace(filePath))
593 throw new ArgumentNullException("filePath");
594 if (!Directory.Exists(filePath))
595 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
596 Contract.EndContractBlock();
598 var pair=(from monitor in Monitors
599 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
600 select monitor).FirstOrDefault();
601 var accountMonitor = pair.Value;
602 var info = accountMonitor.GetContainerInfo(filePath);
606 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
607 _windowManager.ShowWindow(containerProperties);
610 public void SynchNow()
612 _pollAgent.SynchNow();
615 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
617 if (currentInfo==null)
618 throw new ArgumentNullException("currentInfo");
619 Contract.EndContractBlock();
621 var monitor = Monitors[currentInfo.Account];
622 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
626 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
628 if (container == null)
629 throw new ArgumentNullException("container");
630 Contract.EndContractBlock();
632 var monitor = Monitors[container.Account];
633 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
638 public void ToggleSynching()
641 foreach (var pair in Monitors)
643 var monitor = pair.Value;
644 monitor.Pause = !monitor.Pause;
645 isPaused = monitor.Pause;
649 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
650 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
651 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
654 public void ExitPithos()
656 foreach (var pair in Monitors)
658 var monitor = pair.Value;
662 ((Window)GetView()).Close();
667 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
669 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
670 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
671 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
672 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
673 }.ToDictionary(s => s.Status);
675 readonly IWindowManager _windowManager;
677 //private int _syncCount=0;
680 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
682 public void SetPithosStatus(PithosStatus status)
684 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
686 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
688 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
689 _pithosStatus = PithosStatus.InSynch;
691 _pithosStatus = status;
695 public void SetPithosStatus(PithosStatus status,string message)
697 StatusMessage = message;
698 SetPithosStatus(status);
704 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
706 public void UpdateStatus()
709 if (_iconNames.ContainsKey(_pithosStatus))
711 var info = _iconNames[_pithosStatus];
712 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
715 if (_pithosStatus == PithosStatus.InSynch)
716 StatusMessage = "All files up to date";
721 private Task StartMonitor(PithosMonitor monitor,int retries=0)
723 return Task.Factory.StartNew(() =>
725 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
729 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
733 catch (WebException exc)
735 if (AbandonRetry(monitor, retries))
738 HttpStatusCode statusCode =HttpStatusCode.OK;
739 var response = exc.Response as HttpWebResponse;
741 statusCode = response.StatusCode;
745 case HttpStatusCode.Unauthorized:
746 var message = String.Format("API Key Expired for {0}. Starting Renewal",
748 Log.Error(message, exc);
749 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
750 account.IsExpired = true;
751 Notify(new ExpirationNotification(account));
752 //TryAuthorize(monitor.UserName, retries).Wait();
754 case HttpStatusCode.ProxyAuthenticationRequired:
755 TryAuthenticateProxy(monitor,retries);
758 TryLater(monitor, exc, retries);
762 catch (Exception exc)
764 if (AbandonRetry(monitor, retries))
767 TryLater(monitor,exc,retries);
773 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
775 Execute.OnUIThread(() =>
777 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
778 proxyAccount.Settings = Settings;
779 if (true != _windowManager.ShowDialog(proxyAccount))
781 StartMonitor(monitor, retries);
782 NotifyOfPropertyChange(() => Accounts);
786 private bool AbandonRetry(PithosMonitor monitor, int retries)
790 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
792 _events.Publish(new Notification
793 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
800 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
802 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
803 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
804 _events.Publish(new Notification
805 {Title = "Error", Message = message, Level = TraceLevel.Error});
806 Log.Error(message, exc);
810 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
812 StatusMessage = status;
814 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
817 public void NotifyChangedFile(string filePath)
819 if (RecentFiles.Any(e => e.FullPath == filePath))
822 IProducerConsumerCollection<FileEntry> files=RecentFiles;
824 while (files.Count > 5)
825 files.TryTake(out popped);
826 var entry = new FileEntry { FullPath = filePath };
830 public void NotifyAccount(AccountInfo account)
834 //TODO: What happens to an existing account whose Token has changed?
835 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
836 account.SiteUri, Uri.EscapeDataString(account.Token),
837 Uri.EscapeDataString(account.UserName));
839 if (Accounts.All(item => item.UserName != account.UserName))
840 Accounts.TryAdd(account);
844 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
846 if (conflictFiles == null)
848 //Convert to list to avoid multiple iterations
849 var files = conflictFiles.ToList();
854 //TODO: Create a more specific message. For now, just show a warning
855 NotifyForFiles(files,message,TraceLevel.Warning);
859 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
866 StatusMessage = message;
868 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
871 public void Notify(Notification notification)
873 _events.Publish(notification);
877 public void RemoveMonitor(string accountName)
879 if (String.IsNullOrWhiteSpace(accountName))
882 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
883 if (accountInfo != null)
885 _accounts.TryRemove(accountInfo);
886 _pollAgent.RemoveAccount(accountInfo);
889 PithosMonitor monitor;
890 if (Monitors.TryRemove(accountName, out monitor))
893 //TODO: Also remove any pending actions for this account
894 //from the network queue
898 public void RefreshOverlays()
900 foreach (var pair in Monitors)
902 var monitor = pair.Value;
904 var path = monitor.RootPath;
906 if (String.IsNullOrWhiteSpace(path))
909 if (!Directory.Exists(path) && !File.Exists(path))
912 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
916 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
917 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
918 pathPointer, IntPtr.Zero);
922 Marshal.FreeHGlobal(pathPointer);
927 #region Event Handlers
929 public void Handle(SelectiveSynchChanges message)
931 var accountName = message.Account.AccountName;
932 PithosMonitor monitor;
933 if (_monitors.TryGetValue(accountName, out monitor))
935 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
942 private bool _pollStarted;
943 private Sparkle _sparkle;
944 private bool _manualUpgradeCheck;
946 //SMELL: Doing so much work for notifications in the shell is wrong
947 //The notifications should be moved to their own view/viewmodel pair
948 //and different templates should be used for different message types
949 //This will also allow the addition of extra functionality, eg. actions
951 public void Handle(Notification notification)
955 if (!Settings.ShowDesktopNotifications)
958 if (notification is PollNotification)
963 if (notification is CloudNotification)
968 notification.Title = "Pithos+";
969 notification.Message = "Start Synchronisation";
972 var deleteNotification = notification as CloudDeleteNotification;
973 if (deleteNotification != null)
975 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
979 var progress = notification as ProgressNotification;
982 if (progress != null)
984 StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
986 progress.Block/(double)progress.TotalBlocks,
987 progress.FileSize.ToByteSize(),
992 var info = notification as StatusNotification;
995 StatusMessage = info.Title;
998 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
1001 if (notification.Level <= TraceLevel.Warning)
1002 ShowBalloonFor(notification);
1005 private void ShowBalloonFor(Notification notification)
1007 Contract.Requires(notification!=null);
1009 if (!Settings.ShowDesktopNotifications)
1013 switch (notification.Level)
1015 case TraceLevel.Verbose:
1017 case TraceLevel.Info:
1018 icon = BalloonIcon.Info;
1020 case TraceLevel.Error:
1021 icon = BalloonIcon.Error;
1023 case TraceLevel.Warning:
1024 icon = BalloonIcon.Warning;
1030 var tv = (ShellView) GetView();
1031 System.Action clickAction = null;
1032 if (notification is ExpirationNotification)
1034 clickAction = () => ShowPreferences("AccountTab");
1036 var balloon = new PithosBalloon
1038 Title = notification.Title,
1039 Message = notification.Message,
1041 ClickAction = clickAction
1043 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1048 public void Handle(ShowFilePropertiesEvent message)
1050 if (message == null)
1051 throw new ArgumentNullException("message");
1052 if (String.IsNullOrWhiteSpace(message.FileName) )
1053 throw new ArgumentException("message");
1054 Contract.EndContractBlock();
1056 var fileName = message.FileName;
1057 //TODO: Display file properties for non-container folders
1058 if (File.Exists(fileName))
1059 //Retrieve the full name with exact casing. Pithos names are case sensitive
1060 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1061 else if (Directory.Exists(fileName))
1062 //Retrieve the full name with exact casing. Pithos names are case sensitive
1064 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1065 if (IsContainer(path))
1066 ShowContainerProperties(path);
1068 ShowFileProperties(path);
1072 private bool IsContainer(string path)
1074 var matchingFolders = from account in _accounts
1075 from rootFolder in Directory.GetDirectories(account.AccountPath)
1076 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1078 return matchingFolders.Any();
1081 public FileStatus GetFileStatus(string localFileName)
1083 if (String.IsNullOrWhiteSpace(localFileName))
1084 throw new ArgumentNullException("localFileName");
1085 Contract.EndContractBlock();
1087 var statusKeeper = IoC.Get<IStatusKeeper>();
1088 var status=statusKeeper.GetFileStatus(localFileName);
1092 public void RemoveAccountFromDatabase(AccountSettings account)
1094 var statusKeeper = IoC.Get<IStatusKeeper>();
1095 statusKeeper.ClearFolderStatus(account.RootPath);