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. ///