From 4e4046ce4364d4404ef9fdc0ec9396d8866cf092 Mon Sep 17 00:00:00 2001 From: Patrick Simpson Date: Wed, 24 May 2017 15:29:42 +0200 Subject: [PATCH] [KOE-24] Fixed sync dialog to refresh more often, and also updates the ribbon while in progress. Now also records the sync session, to deal with folders that are synced and removed from the list. --- .../AcaciaZPushPlugin.csproj | 1 + .../Controls/KDialogButtons.cs | 12 +- .../AcaciaZPushPlugin/Controls/KDialogNew.cs | 19 + .../Features/SyncState/FeatureSyncState.cs | 330 +++++++++++------- .../Features/SyncState/SyncState.cs | 7 +- .../Features/SyncState/SyncStateDialog.cs | 23 +- .../Features/SyncState/SyncStateDialog.resx | 120 ++++--- .../AcaciaZPushPlugin/Native/User32.cs | 26 ++ .../AcaciaZPushPlugin/OutlookConstants.cs | 5 + .../AcaciaZPushPlugin/Stubs/IAddIn.cs | 2 +- .../AcaciaZPushPlugin/Stubs/ISystemWindow.cs | 15 + .../Stubs/OutlookWrappers/AddInWrapper.cs | 13 +- .../UI/Outlook/CommandElement.cs | 8 +- .../AcaciaZPushPlugin/UI/Outlook/OutlookUI.cs | 7 +- .../UI/Outlook/RibbonToggleButton.cs | 2 +- .../AcaciaZPushPlugin/ZPush/ZPushSync.cs | 264 +++++++++++++- 16 files changed, 660 insertions(+), 194 deletions(-) create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISystemWindow.cs diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj index 53391e2..65c08c9 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj @@ -333,6 +333,7 @@ + diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.cs index 16609e7..adaab68 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.cs @@ -206,15 +206,23 @@ namespace Acacia.Controls private void buttonCancel_Click(object sender, EventArgs e) { - if (Cancellation != null) - Cancellation.Cancel(); + DoClose(); } private void buttonClose_Click(object sender, EventArgs e) + { + DoClose(); + } + + private void DoClose() { if (Cancellation != null) Cancellation.Cancel(); + // If we're not on a modal form, close the form manually + Form form = FindForm(); + if (form?.Modal == false) + form.Close(); } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.cs index da482ee..778e652 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Drawing; using System.Linq; using System.Text; using System.Threading; @@ -32,6 +33,24 @@ namespace Acacia.Controls Icon = Properties.Resources.Kopano; } + /// + /// Show(IWin32Window) doesn't properly center on the parent, this fixes that + /// + /// The owner, used for the bounds + public void ShowCentered(Stubs.ISystemWindow owner) + { + if (owner == null) + { + Show(); + return; + } + Rectangle parent = owner.Bounds; + Rectangle centered = KUIUtil.Center(parent, Size); + StartPosition = FormStartPosition.Manual; + Location = new Point(centered.X - parent.X, centered.Y - parent.Y); + Show(owner); + } + #region Control links [Category("Kopano")] diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/FeatureSyncState.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/FeatureSyncState.cs index 48142a6..cdd96c3 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/FeatureSyncState.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/FeatureSyncState.cs @@ -32,6 +32,8 @@ using Acacia.ZPush.Connect.Soap; using Acacia.Features.GAB; using Acacia.Features.Signatures; using Acacia.UI; +using System.Windows.Forms; +using System.Collections.Concurrent; // Prevent field assignment warnings #pragma warning disable 0649 @@ -45,32 +47,35 @@ namespace Acacia.Features.SyncState #region Sync configuration [AcaciaOption("Sets the period to check synchronisation state if a sync is in progress.")] - public TimeSpan CheckPeriod + public TimeSpan CheckPeriodSync { - get { return GetOption(OPTION_CHECK_PERIOD); } - set { SetOption(OPTION_CHECK_PERIOD, value); } + get { return GetOption(OPTION_CHECK_PERIOD_SYNC); } + set { SetOption(OPTION_CHECK_PERIOD_SYNC, value); } } - private static readonly TimeSpanOption OPTION_CHECK_PERIOD = new TimeSpanOption("CheckPeriod", new TimeSpan(0, 5, 0)); + private static readonly TimeSpanOption OPTION_CHECK_PERIOD_SYNC = new TimeSpanOption("CheckPeriodSync", new TimeSpan(0, 1, 0)); [AcaciaOption("Sets the period to check synchronisation state if a sync is in progress and the dialog is open.")] - public TimeSpan CheckPeriodDialog + public TimeSpan CheckPeriodDialogSync { - get { return GetOption(OPTION_CHECK_PERIOD_DIALOG); } - set { SetOption(OPTION_CHECK_PERIOD_DIALOG, value); } + get { return GetOption(OPTION_CHECK_PERIOD_DIALOG_SYNC); } + set { SetOption(OPTION_CHECK_PERIOD_DIALOG_SYNC, value); } } - private static readonly TimeSpanOption OPTION_CHECK_PERIOD_DIALOG = new TimeSpanOption("CheckPeriodDialog", new TimeSpan(0, 1, 0)); + private static readonly TimeSpanOption OPTION_CHECK_PERIOD_DIALOG_SYNC = new TimeSpanOption("CheckPeriodDialogSync", new TimeSpan(0, 0, 30)); - private TimeSpan DelayTime + [AcaciaOption("Sets the period to check synchronisation state if a sync is NOT in progress and the dialog is open.")] + public TimeSpan CheckPeriodDialogNoSync { - get - { - if (_dialogOpen) - return CheckPeriodDialog; - else - return CheckPeriod; - } + get { return GetOption(OPTION_CHECK_PERIOD_DIALOG_NO_SYNC); } + set { SetOption(OPTION_CHECK_PERIOD_DIALOG_NO_SYNC, value); } } - private bool _dialogOpen; + private static readonly TimeSpanOption OPTION_CHECK_PERIOD_DIALOG_NO_SYNC = new TimeSpanOption("CheckPeriodDialogNoSync", new TimeSpan(0, 5, 0)); + + private bool _isSyncing; + public TimeSpan CheckPeriodDialogEffective + { + get { return _isSyncing ? CheckPeriodDialogSync : CheckPeriodDialogNoSync; } + } + #endregion @@ -79,39 +84,21 @@ namespace Acacia.Features.SyncState { private FeatureSyncState _feature; - /// - /// Number in range [0,1] - /// - private double _syncProgress = 1; - public double SyncProgress - { - get { return _syncProgress; } - set - { - int old = SyncProgressPercent; - - _syncProgress = value; - - if (SyncProgressPercent != old) - { - // Percentage has changed, update required - _feature._button.Invalidate(); - } - } - } - + private int _syncProgressPercent = 100; public int SyncProgressPercent { - get + get { return _syncProgressPercent; } + set { - return AlignProgress(100); - } - } + if (value != _syncProgressPercent) + { + _syncProgressPercent = Math.Max(0, Math.Min(100, value)); - private int AlignProgress(int steps, double round = 0.5) - { - int val = (int)Math.Floor(_syncProgress * steps + round); - return Math.Min(steps, val); + // Percentage has changed, update required + // If the dialog is open, force the update, otherwise it'll trigger only when the main window is active + _feature._button.Invalidate(_feature._dialog != null); + } + } } public SyncStateData(FeatureSyncState feature) @@ -122,6 +109,7 @@ namespace Acacia.Features.SyncState private static readonly Bitmap[] PROGRESS = CreateProgressImages(); private const int PROGRESS_STEPS = 20; + private static Bitmap[] CreateProgressImages() { Bitmap[] images = new Bitmap[PROGRESS_STEPS + 1]; @@ -151,7 +139,9 @@ namespace Acacia.Features.SyncState public Bitmap GetImage(string elementId, bool large) { - int index = AlignProgress(PROGRESS_STEPS, 0.05); + double round = (double)PROGRESS_STEPS / 100; + int val = (int)Math.Floor((_syncProgressPercent / (double)100) * PROGRESS_STEPS + round); + int index = Math.Max(0, Math.Min(PROGRESS_STEPS, val)); // extra safety check, just in case return (index >= 0 && index <= PROGRESS_STEPS) ? PROGRESS[index] : PROGRESS[0]; @@ -177,6 +167,7 @@ namespace Acacia.Features.SyncState private RibbonButton _button; private SyncStateData _state; + private ZPushSync.SyncTask _task; public FeatureSyncState() { @@ -188,7 +179,7 @@ namespace Acacia.Features.SyncState _button = RegisterButton(this, "Progress", true, ShowSyncState, ZPushBehaviour.None); _button.DataProvider = _state; // Add a sync task to start checking. If this finds it's not fully synchronised, it will check more often - Watcher.Sync.AddTask(this, Name, CheckSyncState); + _task = Watcher.Sync.AddTask(this, Name, CheckSyncState); } private class DeviceDetails : ISoapSerializable @@ -204,6 +195,12 @@ namespace Acacia.Features.SyncState { return string.Format("{0}: {1}/{2}={3}", status, total, done, todo); } + + internal void MakeDone() + { + done = total; + todo = 0; + } } [SoapField] @@ -221,7 +218,7 @@ namespace Acacia.Features.SyncState [SoapField(4)] public SyncData Sync; - public bool IsSyncing { get { return Sync != null; } } + public bool IsSyncing { get { return Sync != null && Sync.todo > 0; } } [SoapField(5)] public string id; // TODO: backend folder id @@ -279,56 +276,97 @@ namespace Acacia.Features.SyncState { get { return _data.data.contentdata; } } - - #region Totals - - /// - /// Calculates the totals for the data - /// - internal void Calculate() - { - long total = 0; - long done = 0; - IsSyncing = false; - foreach (ContentData content in Content.Values) - { - if (content.IsSyncing) - { - total += content.Sync.total; - done += content.Sync.done; - IsSyncing = true; - } - } - - this.Total = total; - this.Done = done; - } - - public long Total - { - get; - private set; - } - - public long Done - { - get; - private set; - } - - public bool IsSyncing - { - get; - private set; - } - - #endregion } private class GetDeviceDetailsRequest : SoapRequest { } + /// + /// Information stored per account on a synchronisation session. + /// + private class SyncSession + { + public long Total { get; private set; } + public long Done { get; private set; } + public bool IsSyncing { get; private set; } + + private readonly FeatureSyncState _feature; + private readonly ZPushAccount _account; + private readonly Dictionary _syncContent = new Dictionary(); + + public SyncSession(FeatureSyncState feature, ZPushAccount account) + { + this._feature = feature; + this._account = account; + } + + /// + /// Adds the details to the current session + /// + public void Add(DeviceDetails details) + { + StringBuilder debug = new StringBuilder(); + + // If a folder is no longer reported as it's already synced, we don't necessarily get the + // last step where (done == total). This causes some folders to keep lingering. To clean these + // up, keep a list of folders syncing in this iteration. + // Any folders in the session but not this list are done + HashSet syncingNow = new HashSet(); + + // Check all syncing data + foreach (DeviceDetails.ContentData content in details.Content.Values) + { + if (content.IsSyncing) + { + // If the current session is not syncing, this is a restart. Clear stat + if (!IsSyncing) + { + _syncContent.Clear(); + debug.AppendLine("Starting new SyncSession"); + } + + // Add to the syncing content + IsSyncing = true; + _syncContent[content.synckey] = content; + syncingNow.Add(content.synckey); + + debug.AppendLine(string.Format("\tFolder: {0} \tSync: {1} \tStatus: {2} / {3}", + content.synckey, content.IsSyncing, content.Sync.done, content.Sync.total)); + } + } + debug.AppendLine(string.Format("Calculating totals: ({0})", IsSyncing)); + + // Clean up any done items + bool _syncingNow = false; + foreach (DeviceDetails.ContentData content in _syncContent.Values) + { + if (!syncingNow.Contains(content.synckey)) + { + content.Sync.MakeDone(); + } + else + { + _syncingNow = true; + } + } + IsSyncing = _syncingNow; + + // Update totals + Total = 0; + Done = 0; + foreach(DeviceDetails.ContentData content in _syncContent.Values) + { + Total += content.Sync.total; + Done += content.Sync.done; + debug.AppendLine(string.Format("\tFolder: {0} \tSync: {1} \tStatus: {2} / {3}", + content.synckey, content.IsSyncing, content.Sync.done, content.Sync.total)); + } + debug.AppendLine(string.Format("Total: {0} / {1} ({2}%)", Done, Total, CalculatePercentage(Done, Total))); + Logger.Instance.Trace(_feature, "Syncing account {0}:\n{1}", _account, debug); + } + } + private void CheckSyncState(ZPushAccount account) { // TODO: we probably want one invocation for all accounts @@ -337,16 +375,28 @@ namespace Acacia.Features.SyncState { // Fetch DeviceDetails details = deviceService.Execute(new GetDeviceDetailsRequest()); + if (details != null) + { + bool wasSyncing = false; - // Determine the totals - details?.Calculate(); + // Create or update session + SyncSession session = account.GetFeatureData(this, null); + if (session == null) + session = new SyncSession(this, account); + else + wasSyncing = session.IsSyncing; - // And store with the account - account.SetFeatureData(this, null, details); + session.Add(details); - // If syncing, check again soon. - if (details?.IsSyncing == true) - Util.Delayed(this, (int)DelayTime.TotalMilliseconds, () => CheckSyncState(account)); + // Store with the account + account.SetFeatureData(this, null, session); + + if (wasSyncing != session.IsSyncing) + { + // Sync state has changed, update the schedule + Watcher.Sync.SetTaskSchedule(_task, account, session.IsSyncing ? CheckPeriodSync : (TimeSpan?)null); + } + } } // Update the total for all accounts @@ -357,35 +407,66 @@ namespace Acacia.Features.SyncState { long total = 0; long done = 0; + bool isSyncing = false; foreach(ZPushAccount account in Watcher.Accounts.GetAccounts()) { - DeviceDetails details = account.GetFeatureData(this, null); - if (details != null) + SyncSession sync = account.GetFeatureData(this, null); + if (sync != null) { - total += details.Total; - done += details.Done; + total += sync.Total; + done += sync.Done; + if (sync.IsSyncing) + isSyncing = true; } } - // Calculate progress and update - if (done == 0) - _state.SyncProgress = total == 0 ? 1 : 0; - else - _state.SyncProgress = (double)done / total; + // Update UI + _state.SyncProgressPercent = CalculatePercentage(done, total); + if (_dialog != null) + _dialog.RefreshData(); + + if (_isSyncing != isSyncing) + { + _isSyncing = isSyncing; + if(_dialog != null) + { + // Update the task schedule + Watcher.Sync.SetTaskSchedule(_task, null, CheckPeriodDialogEffective, false); + } + } } + private static int CalculatePercentage(long done, long total) + { + if (total == 0 || done == total) + return 100; + + return Math.Max(0, Math.Min(100, (int)(done * 100 / total))); + } + + private SyncStateDialog _dialog; + private void ShowSyncState() { - _dialogOpen = true; - try + // Only show the dialog once + if (_dialog != null) + return; + + // Ramp up the checking schedule while the dialog is open + // The other check sets per-account schedules, we use the global one, so they should't interfere. + TimeSpan? old = Watcher.Sync.SetTaskSchedule(_task, null, CheckPeriodDialogEffective, true); + SyncStateDialog dlg = new SyncStateDialog(this); + dlg.FormClosed += (s, e) => { - new SyncStateDialog(this).ShowDialog(); - } - finally - { - _dialogOpen = false; - } + // Restore the schedule + Watcher.Sync.SetTaskSchedule(_task, null, old); + _dialog = null; + }; + + // Show the dialog as a non-modal, otherwise the ribbon doesn't get updated + _dialog = dlg; + dlg.ShowCentered(ThisAddIn.Instance.Window); } private class SyncStateImpl : SyncState @@ -433,6 +514,11 @@ namespace Acacia.Features.SyncState private set; } + public int Percentage + { + get { return CalculatePercentage(Done, Total); } + } + public bool CanResync(ResyncOption option) { // TODO: check if outlook is not offline? @@ -500,12 +586,12 @@ namespace Acacia.Features.SyncState foreach (ZPushAccount account in _accounts) { - DeviceDetails details = account.GetFeatureData(_feature, null); - if (details != null) + SyncSession sync = account.GetFeatureData(_feature, null); + if (sync != null) { - Total += details.Total; - Done += details.Done; - if (details.IsSyncing) + Total += sync.Total; + Done += sync.Done; + if (sync.IsSyncing) IsSyncing = true; } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncState.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncState.cs index e91c92e..2ec3aff 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncState.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncState.cs @@ -24,10 +24,15 @@ namespace Acacia.Features.SyncState long Remaining { get; } /// - /// Returns the number of items already synced; + /// Returns the number of items already synced. /// long Done { get; } + /// + /// Returns the percentage of syncing that is done. + /// + int Percentage { get; } + bool IsSyncing { get; } /// diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncStateDialog.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncStateDialog.cs index 573c5fa..bf9ebd7 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncStateDialog.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncStateDialog.cs @@ -50,15 +50,6 @@ namespace Acacia.Features.SyncState // Add the accounts foreach (ZPushAccount account in ThisAddIn.Instance.Watcher.Accounts.GetAccounts()) comboAccounts.Items.Add(account); - - // Add a timer to update the UI - Timer timer = new Timer(); - timer.Interval = 2500; - timer.Tick += (o, args) => - { - UpdateUI(); - }; - timer.Start(); } private void ShowHint(object sender, KHintButton.HintEventArgs e) @@ -136,9 +127,21 @@ namespace Acacia.Features.SyncState _syncButtons[(int)option].Enabled = _syncState.CanResync(option); } + RefreshDisplay(); + } + + public void RefreshData() + { + _syncState.Update(); + ThisAddIn.Instance.InUI(RefreshDisplay); + } + + private void RefreshDisplay() + { if (_syncState.IsSyncing) { - textRemaining.Text = _syncState.Remaining.ToString() + " / " + _syncState.Total.ToString(); + textRemaining.Text = string.Format("{0} / {1} ({2}%)", _syncState.Done, _syncState.Total, + _syncState.Percentage); progress.Value = (int)(_syncState.Done * 100.0 / _syncState.Total); } else diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncStateDialog.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncStateDialog.resx index 2db0c22..a9dc4f6 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncStateDialog.resx +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SyncState/SyncStateDialog.resx @@ -139,13 +139,13 @@ - 2, 286 + 5, 711 - 2, 2, 2, 2 + 5, 5, 5, 5 - 333, 35 + 889, 54 0 @@ -181,10 +181,13 @@ NoControl - 3, 54 + 8, 129 + + + 8, 0, 8, 0 - 77, 25 + 203, 60 5 @@ -214,10 +217,13 @@ Fill - 3, 0 + 8, 0 + + + 8, 0, 8, 0 - 77, 27 + 203, 53 0 @@ -250,10 +256,13 @@ All Z-Push accounts - 86, 3 + 227, 7 + + + 8, 7, 8, 7 - 242, 21 + 648, 39 1 @@ -277,10 +286,13 @@ Fill - 3, 27 + 8, 53 + + + 8, 0, 8, 0 - 77, 27 + 203, 76 2 @@ -307,10 +319,13 @@ Fill - 86, 30 + 227, 60 + + + 8, 7, 8, 7 - 242, 21 + 648, 62 3 @@ -334,16 +349,16 @@ Fill - 86, 57 + 227, 136 - 3, 3, 3, 3 + 8, 7, 8, 7 - 3, 3, 3, 3 + 8, 7, 8, 7 - 242, 19 + 648, 46 7 @@ -367,10 +382,13 @@ Fill - 3, 79 + 8, 189 + + + 8, 0, 8, 0 - 77, 35 + 203, 70 8 @@ -409,13 +427,16 @@ NoControl - 86, 82 + 227, 196 + + + 8, 7, 8, 7 - 3, 3, 3, 3 + 8, 7, 8, 7 - 242, 29 + 648, 56 0 @@ -451,13 +472,16 @@ NoControl - 86, 117 + 227, 266 + + + 8, 7, 8, 7 - 3, 3, 3, 3 + 8, 7, 8, 7 - 242, 29 + 648, 56 1 @@ -493,13 +517,16 @@ NoControl - 86, 152 + 227, 336 + + + 8, 7, 8, 7 - 3, 3, 3, 3 + 8, 7, 8, 7 - 242, 29 + 648, 56 2 @@ -535,13 +562,16 @@ NoControl - 86, 187 + 227, 406 + + + 8, 7, 8, 7 - 3, 3, 3, 3 + 8, 7, 8, 7 - 242, 29 + 648, 56 3 @@ -565,13 +595,16 @@ Fill - 86, 219 + 227, 469 + + + 8, 0, 8, 0 - 0, 6, 0, 0 + 0, 14, 0, 0 - 242, 59 + 648, 223 4 @@ -592,13 +625,16 @@ Fill - 3, 3 + 8, 7 + + + 8, 7, 8, 7 8 - 331, 278 + 883, 692 1 @@ -624,11 +660,14 @@ 0, 0 + + 8, 7, 8, 7 + 2 - 337, 323 + 899, 770 0 @@ -652,13 +691,16 @@ True - 6, 13 + 16, 31 True - 337, 323 + 899, 770 + + + 8, 7, 8, 7 CenterParent diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs index 4eb05ee..1499612 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -47,6 +48,31 @@ namespace Acacia.Native [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr hWnd, GWL gwl); + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + + public Rectangle ToRectangle() + { + return new Rectangle(Left, Top, Right - Left, Bottom - Top); + } + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + public static Rectangle GetWindowRect(IntPtr hWnd) + { + RECT rect; + GetWindowRect(hWnd, out rect); + return rect.ToRectangle(); + } + #region Messages [DllImport("user32.dll")] diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs index 308a47e..b682be8 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs @@ -116,6 +116,11 @@ namespace Acacia public const string PR_ORIGINATOR_NON_DELIVERY_REPORT_REQUESTED = PROP + "0C08" + PT_BOOLEAN; public const string PR_READ_RECEIPT_REQUESTED = PROP + "0029" + PT_BOOLEAN; + public const string PR_READ_RECEIPT_ADDR_TYPE = PROP + "4029" + PT_UNICODE; + public const string PR_READ_RECEIPT_DISPLAY_NAME = PROP + "402B" + PT_UNICODE; + public const string PR_READ_RECEIPT_EMAIL_ADDR = PROP + "402A" + PT_UNICODE; + public const string PR_READ_RECEIPT_SIMPLE_DISP_NAME = PROP + "4036" + PT_UNICODE; + #endregion #region Meeting requests diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddIn.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddIn.cs index be29bea..87d7110 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddIn.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddIn.cs @@ -40,7 +40,7 @@ namespace Acacia.Stubs #region UI OutlookUI OutlookUI { get; } - IWin32Window Window { get; } + ISystemWindow Window { get; } IExplorer GetActiveExplorer(); #endregion diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISystemWindow.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISystemWindow.cs new file mode 100644 index 0000000..e7fdbee --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISystemWindow.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Acacia.Stubs +{ + public interface ISystemWindow : IWin32Window + { + Rectangle Bounds { get; } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs index e9233cc..33b2d01 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs @@ -29,6 +29,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using NSOutlook = Microsoft.Office.Interop.Outlook; +using System.Drawing; namespace Acacia.Stubs.OutlookWrappers { @@ -259,7 +260,7 @@ namespace Acacia.Stubs.OutlookWrappers /// Simple IWin32Window wrapper for a native handle. NativeWindow sometimes refuses to handle /// these (FromHandle returns null), so use a simple wrapper. /// - private class WindowHandle : IWin32Window + private class WindowHandle : ISystemWindow { private IntPtr hWnd; @@ -268,6 +269,14 @@ namespace Acacia.Stubs.OutlookWrappers this.hWnd = hWnd; } + public Rectangle Bounds + { + get + { + return User32.GetWindowRect(hWnd); + } + } + public IntPtr Handle { get @@ -277,7 +286,7 @@ namespace Acacia.Stubs.OutlookWrappers } } - public IWin32Window Window + public ISystemWindow Window { get { diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/CommandElement.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/CommandElement.cs index 85748f2..8a9f5c9 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/CommandElement.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/CommandElement.cs @@ -67,9 +67,13 @@ namespace Acacia.UI.Outlook Logger.Instance.Trace(Owner, "Command {0}: Handled", Id); } - public void Invalidate() + /// + /// Invalidates the command, triggering a reload of labels and images. + /// + /// If true, the Outlook UI will be updated straight away. + public void Invalidate(bool forceUpdate = false) { - UI?.InvalidateCommand(this); + UI?.InvalidateCommand(this, forceUpdate); } private bool _isEnabled = true; diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/OutlookUI.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/OutlookUI.cs index 6fb19e8..a3fa14d 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/OutlookUI.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/OutlookUI.cs @@ -327,9 +327,14 @@ namespace Acacia.UI.Outlook #region Command state - internal void InvalidateCommand(CommandElement command) + internal void InvalidateCommand(CommandElement command, bool forceUpdate) { _officeUI?.InvalidateControl(command.Id); + if (forceUpdate) + { + _officeUI?.Invalidate(); + // TODO: sometimes if the focus is on another window, it is not updated. + } } public bool getControlEnabled(Office.IRibbonControl control) diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/RibbonToggleButton.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/RibbonToggleButton.cs index e8eefa4..97d1406 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/RibbonToggleButton.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/RibbonToggleButton.cs @@ -46,7 +46,7 @@ namespace Acacia.UI.Outlook if (_isPressed != value) { _isPressed = value; - UI?.InvalidateCommand(this); + UI?.InvalidateCommand(this, false); } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushSync.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushSync.cs index 028942a..7efdc06 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushSync.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushSync.cs @@ -22,8 +22,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms; namespace Acacia.ZPush { @@ -34,18 +34,89 @@ namespace Acacia.ZPush { #region SyncTask + public interface SyncTask + { + + } + + private class Schedule + { + public delegate void TickHandler(Schedule schedule); + public readonly SyncTaskImpl Task; + public readonly ZPushAccount Account; + private readonly TickHandler _tick; + private TimeSpan _period; + + /// + /// We use a Threading.Timer here, as the schedule may be modified from any thread + /// + private Timer _timer; + + public TimeSpan Period + { + get { return _period; } + } + + public Schedule(SyncTaskImpl task, ZPushAccount account, TickHandler scheduleTick, TimeSpan period) + { + this.Task = task; + this.Account = account; + this._tick = scheduleTick; + this._period = period; + } + + private void _timer_Tick(object state) + { + _tick(this); + } + + public void Cancel() + { + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } + } + + public void SetPeriod(TimeSpan value, bool executeNow) + { + _period = value; + + // Start, this will destroy the old and start the new + Start(executeNow); + } + + public void Start(bool executeNow) + { + // Cancel any existing timer + Cancel(); + _timer = new Timer(_timer_Tick, null, executeNow ? TimeSpan.Zero : _period, _period); + } + } + /// /// Represents a SyncTask. This is not specific for an account. When tasks are executed, GetInstance is /// used to create a task instance for each account, which will be executed by the system task manager. /// - private class SyncTask + private class SyncTaskImpl : SyncTask { private readonly Feature _owner; private readonly string _name; private readonly SyncAction _action; private readonly SyncActionConnection _actionConnection; - public SyncTask(Feature owner, string name, SyncAction action, SyncActionConnection actionConnection) + /// + /// Optional schedule for all accounts. + /// + private Schedule _scheduleAll; + + /// + /// Optional schedules per account. + /// + private readonly Dictionary _schedulesAccount = new Dictionary(); + + public SyncTaskImpl(Feature owner, string name, SyncAction action, SyncActionConnection actionConnection) { this._owner = owner; this._name = name; @@ -53,8 +124,25 @@ namespace Acacia.ZPush this._actionConnection = actionConnection; } - public AcaciaTask GetInstance(ZPushAccount account) + public override string ToString() { + string s = _owner.Name; + if (_name != null) + s += ":" + _name; + return s; + } + + /// + /// Returns the task for execution. + /// + /// The schedule that triggered the task execution, or null if this is a sync. + /// The account for which the tasks are requested. + /// The task instance, or null if there is no task to execute in this schedule. + public AcaciaTask GetInstance(Schedule schedule, ZPushAccount account) + { + if (!IsTaskScheduleApplicable(schedule, account)) + return null; + if (_actionConnection != null) { return new AcaciaTask(null, _owner, _name, () => @@ -71,6 +159,68 @@ namespace Acacia.ZPush return new AcaciaTask(null, _owner, _name, () => _action(account)); } } + + + /// + /// Sets a custom schedule for the task. This method only records the schedule, execution is done by ZPushSync. + /// + /// The specific account to set the schedule for, or null to set it for all accounts + /// The schedule. Specify null to clear the schedule and resume the original. + public void SetTaskSchedule(ZPushAccount account, Schedule schedule) + { + if (account == null) + _scheduleAll = schedule; + else + { + if (schedule == null) + _schedulesAccount.Remove(account); + else + _schedulesAccount[account] = schedule; + } + } + + /// + /// Returns any custom task scheddule registered for the account, or all accounts. + /// + /// The account, or null to retrieve the schedule for all accounts. + /// The schedule, or null if no schedule is registered + public Schedule GetTaskSchedule(ZPushAccount account) + { + if (account == null) + { + return _scheduleAll; + } + else + { + Schedule schedule; + _schedulesAccount.TryGetValue(account, out schedule); + return schedule; + } + } + + private bool IsTaskScheduleApplicable(Schedule schedule, ZPushAccount account) + { + // The fastest schedule is applicable, so determine that + Schedule perAccount = GetTaskSchedule(account); + Schedule all = _scheduleAll; + + // If there is no custom schedule, only applicable if the task schedule is the standard sync + if (perAccount == null && all == null) + return schedule == null; + // Otherwise the standard sync is not applicable + else if (schedule == null) + return false; + + // Determine the quickest + Schedule quickest = perAccount; + if (quickest == null) + quickest = all; + else if (all != null && all.Period.TotalMilliseconds < quickest.Period.TotalMilliseconds) + quickest = all; + + // Applicable if that is the effective schedule + return quickest == schedule; + } } #endregion @@ -78,10 +228,10 @@ namespace Acacia.ZPush #region Setup private readonly ISyncObject _syncObject; - private readonly Timer _timer; + private readonly System.Windows.Forms.Timer _timer; private ZPushWatcher _watcher; private bool _started; - private readonly List _tasks = new List(); + private readonly List _tasks = new List(); public readonly bool Enabled; public readonly TimeSpan Period; @@ -103,7 +253,7 @@ namespace Acacia.ZPush if (Enabled) { _watcher = watcher; - _timer = new Timer(); + _timer = new System.Windows.Forms.Timer(); _timer.Interval = (int)Period.TotalMilliseconds; _timer.Tick += _timer_Tick; _timer.Start(); @@ -153,11 +303,13 @@ namespace Acacia.ZPush /// The feature owning the task. /// The task's name, for logging. /// The action to execute. - public void AddTask(Feature owner, string name, SyncAction action) + public SyncTask AddTask(Feature owner, string name, SyncAction action) { if (_started) throw new Exception("Already started, cannot add task"); - _tasks.Add(new SyncTask(owner, name, action, null)); + SyncTaskImpl task = new SyncTaskImpl(owner, name, action, null); + _tasks.Add(task); + return task; } @@ -167,11 +319,87 @@ namespace Acacia.ZPush /// The feature owning the task. /// The task's name, for logging. /// The action to execute. - public void AddTask(Feature owner, string name, SyncActionConnection action) + public SyncTask AddTask(Feature owner, string name, SyncActionConnection action) { if (_started) throw new Exception("Already started, cannot add task"); - _tasks.Add(new SyncTask(owner, name, null, action)); + + SyncTaskImpl task = new SyncTaskImpl(owner, name, null, action); + _tasks.Add(task); + return task; + } + + /// + /// Sets a custom schedule for the specified task. + /// + /// The task + /// The specific account to set the schedule for, or null to set it for all accounts. + /// If a per-account schedule is set, and an all-account schedule, the quickest of both applies. + /// The schedule. Specify null to clear the schedule and resume the original. + /// The old schedule, or null if there was none. + public TimeSpan? SetTaskSchedule(SyncTask task, ZPushAccount account, TimeSpan? period, bool executeNow = false) + { + SyncTaskImpl impl = (SyncTaskImpl)task; + + Logger.Instance.Trace(this, "Setting task schedule for {0}{1} to {2}", + impl.ToString(), + account == null ? "" : (" for account " + account), + period == null ? "default" : period.Value.ToString()); + + // Check if there's an existing schedule to modify + Schedule schedule = impl.GetTaskSchedule(account); + TimeSpan? old = schedule?.Period; + + if (period == null) + { + // Clearing the schedule + if (schedule != null) + { + // Clear the existing schedule + schedule.Cancel(); + impl.SetTaskSchedule(account, null); + } + // If there was no schedule, we're already done + } + else + { + // Setting the schedule + if (schedule == null) + { + // Create a new schedule + schedule = new Schedule(impl, account, ScheduleTick, period.Value); + impl.SetTaskSchedule(account, schedule); + schedule.Start(executeNow); + } + else + { + // Update the existing schedule + schedule.SetPeriod(period.Value, executeNow); + } + } + + return old; + } + + private void ScheduleTick(Schedule schedule) + { + // Don't contact the network if Outlook is offline + if (ThisAddIn.Instance.IsOffline) + return; + + if (schedule.Account == null) + { + // Execute tasks for all accounts + foreach (ZPushAccount account in _watcher.Accounts.GetAccounts()) + { + ExecuteTask(schedule.Task.GetInstance(schedule, account), false); + } + } + else + { + // Execute tasks for the specific account + ExecuteTask(schedule.Task.GetInstance(schedule, schedule.Account), false); + } } #endregion @@ -218,12 +446,22 @@ namespace Acacia.ZPush return; // Execute the tasks for the account - foreach (SyncTask task in _tasks) + foreach (SyncTaskImpl task in _tasks) { - Tasks.Task(task.GetInstance(account), synchronous); + ExecuteTask(task.GetInstance(null, account), synchronous); } } + /// + /// Helper to execute a task. Silently ignores null tasks. + /// + private void ExecuteTask(AcaciaTask task, bool synchronous) + { + if (task == null) + return; + Tasks.Task(task, synchronous); + } + /// /// Timer callback, executes any tasks. ///