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