1
0
mirror of https://github.com/Kopano-dev/kopano-ol-extension.git synced 2023-10-10 13:37:40 +02:00

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

This commit is contained in:
Patrick Simpson 2017-05-24 15:29:42 +02:00
parent ef92888586
commit 4e4046ce43
16 changed files with 660 additions and 194 deletions

View File

@ -333,6 +333,7 @@
<Compile Include="Stubs\ISignatures.cs" />
<Compile Include="Stubs\IStores.cs" />
<Compile Include="Stubs\ISyncObject.cs" />
<Compile Include="Stubs\ISystemWindow.cs" />
<Compile Include="Stubs\OutlookWrappers\AccountWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\AddInWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\AddressEntryWrapper.cs" />

View File

@ -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();
}
}
}

View File

@ -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;
}
/// <summary>
/// Show(IWin32Window) doesn't properly center on the parent, this fixes that
/// </summary>
/// <param name="owner">The owner, used for the bounds</param>
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")]

View File

@ -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;
/// <summary>
/// Number in range [0,1]
/// </summary>
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<DeviceDetails.Data>
@ -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
/// <summary>
/// Calculates the totals for the data
/// </summary>
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<DeviceDetails>
{
}
/// <summary>
/// Information stored per account on a synchronisation session.
/// </summary>
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<string, DeviceDetails.ContentData> _syncContent = new Dictionary<string, DeviceDetails.ContentData>();
public SyncSession(FeatureSyncState feature, ZPushAccount account)
{
this._feature = feature;
this._account = account;
}
/// <summary>
/// Adds the details to the current session
/// </summary>
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<string> syncingNow = new HashSet<string>();
// 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<SyncSession>(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<DeviceDetails>(this, null);
if (details != null)
SyncSession sync = account.GetFeatureData<SyncSession>(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<DeviceDetails>(_feature, null);
if (details != null)
SyncSession sync = account.GetFeatureData<SyncSession>(_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;
}
}

View File

@ -24,10 +24,15 @@ namespace Acacia.Features.SyncState
long Remaining { get; }
/// <summary>
/// Returns the number of items already synced;
/// Returns the number of items already synced.
/// </summary>
long Done { get; }
/// <summary>
/// Returns the percentage of syncing that is done.
/// </summary>
int Percentage { get; }
bool IsSyncing { get; }
/// <summary>

View File

@ -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

View File

@ -139,13 +139,13 @@
</data>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="_buttons.Location" type="System.Drawing.Point, System.Drawing">
<value>2, 286</value>
<value>5, 711</value>
</data>
<data name="_buttons.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>2, 2, 2, 2</value>
<value>5, 5, 5, 5</value>
</data>
<data name="_buttons.Size" type="System.Drawing.Size, System.Drawing">
<value>333, 35</value>
<value>889, 54</value>
</data>
<data name="_buttons.TabIndex" type="System.Int32, mscorlib">
<value>0</value>
@ -181,10 +181,13 @@
<value>NoControl</value>
</data>
<data name="_labelRemaining.Location" type="System.Drawing.Point, System.Drawing">
<value>3, 54</value>
<value>8, 129</value>
</data>
<data name="_labelRemaining.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 0, 8, 0</value>
</data>
<data name="_labelRemaining.Size" type="System.Drawing.Size, System.Drawing">
<value>77, 25</value>
<value>203, 60</value>
</data>
<data name="_labelRemaining.TabIndex" type="System.Int32, mscorlib">
<value>5</value>
@ -214,10 +217,13 @@
<value>Fill</value>
</data>
<data name="_labelAccount.Location" type="System.Drawing.Point, System.Drawing">
<value>3, 0</value>
<value>8, 0</value>
</data>
<data name="_labelAccount.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 0, 8, 0</value>
</data>
<data name="_labelAccount.Size" type="System.Drawing.Size, System.Drawing">
<value>77, 27</value>
<value>203, 53</value>
</data>
<data name="_labelAccount.TabIndex" type="System.Int32, mscorlib">
<value>0</value>
@ -250,10 +256,13 @@
<value>All Z-Push accounts</value>
</data>
<data name="comboAccounts.Location" type="System.Drawing.Point, System.Drawing">
<value>86, 3</value>
<value>227, 7</value>
</data>
<data name="comboAccounts.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="comboAccounts.Size" type="System.Drawing.Size, System.Drawing">
<value>242, 21</value>
<value>648, 39</value>
</data>
<data name="comboAccounts.TabIndex" type="System.Int32, mscorlib">
<value>1</value>
@ -277,10 +286,13 @@
<value>Fill</value>
</data>
<data name="_labelProgress.Location" type="System.Drawing.Point, System.Drawing">
<value>3, 27</value>
<value>8, 53</value>
</data>
<data name="_labelProgress.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 0, 8, 0</value>
</data>
<data name="_labelProgress.Size" type="System.Drawing.Size, System.Drawing">
<value>77, 27</value>
<value>203, 76</value>
</data>
<data name="_labelProgress.TabIndex" type="System.Int32, mscorlib">
<value>2</value>
@ -307,10 +319,13 @@
<value>Fill</value>
</data>
<data name="progress.Location" type="System.Drawing.Point, System.Drawing">
<value>86, 30</value>
<value>227, 60</value>
</data>
<data name="progress.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="progress.Size" type="System.Drawing.Size, System.Drawing">
<value>242, 21</value>
<value>648, 62</value>
</data>
<data name="progress.TabIndex" type="System.Int32, mscorlib">
<value>3</value>
@ -334,16 +349,16 @@
<value>Fill</value>
</data>
<data name="textRemaining.Location" type="System.Drawing.Point, System.Drawing">
<value>86, 57</value>
<value>227, 136</value>
</data>
<data name="textRemaining.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>3, 3, 3, 3</value>
<value>8, 7, 8, 7</value>
</data>
<data name="textRemaining.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>3, 3, 3, 3</value>
<value>8, 7, 8, 7</value>
</data>
<data name="textRemaining.Size" type="System.Drawing.Size, System.Drawing">
<value>242, 19</value>
<value>648, 46</value>
</data>
<data name="textRemaining.TabIndex" type="System.Int32, mscorlib">
<value>7</value>
@ -367,10 +382,13 @@
<value>Fill</value>
</data>
<data name="_labelResync.Location" type="System.Drawing.Point, System.Drawing">
<value>3, 79</value>
<value>8, 189</value>
</data>
<data name="_labelResync.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 0, 8, 0</value>
</data>
<data name="_labelResync.Size" type="System.Drawing.Size, System.Drawing">
<value>77, 35</value>
<value>203, 70</value>
</data>
<data name="_labelResync.TabIndex" type="System.Int32, mscorlib">
<value>8</value>
@ -409,13 +427,16 @@
<value>NoControl</value>
</data>
<data name="buttonGAB.Location" type="System.Drawing.Point, System.Drawing">
<value>86, 82</value>
<value>227, 196</value>
</data>
<data name="buttonGAB.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="buttonGAB.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>3, 3, 3, 3</value>
<value>8, 7, 8, 7</value>
</data>
<data name="buttonGAB.Size" type="System.Drawing.Size, System.Drawing">
<value>242, 29</value>
<value>648, 56</value>
</data>
<data name="buttonGAB.TabIndex" type="System.Int32, mscorlib">
<value>0</value>
@ -451,13 +472,16 @@
<value>NoControl</value>
</data>
<data name="buttonSignatures.Location" type="System.Drawing.Point, System.Drawing">
<value>86, 117</value>
<value>227, 266</value>
</data>
<data name="buttonSignatures.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="buttonSignatures.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>3, 3, 3, 3</value>
<value>8, 7, 8, 7</value>
</data>
<data name="buttonSignatures.Size" type="System.Drawing.Size, System.Drawing">
<value>242, 29</value>
<value>648, 56</value>
</data>
<data name="buttonSignatures.TabIndex" type="System.Int32, mscorlib">
<value>1</value>
@ -493,13 +517,16 @@
<value>NoControl</value>
</data>
<data name="buttonServerData.Location" type="System.Drawing.Point, System.Drawing">
<value>86, 152</value>
<value>227, 336</value>
</data>
<data name="buttonServerData.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="buttonServerData.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>3, 3, 3, 3</value>
<value>8, 7, 8, 7</value>
</data>
<data name="buttonServerData.Size" type="System.Drawing.Size, System.Drawing">
<value>242, 29</value>
<value>648, 56</value>
</data>
<data name="buttonServerData.TabIndex" type="System.Int32, mscorlib">
<value>2</value>
@ -535,13 +562,16 @@
<value>NoControl</value>
</data>
<data name="buttonFullResync.Location" type="System.Drawing.Point, System.Drawing">
<value>86, 187</value>
<value>227, 406</value>
</data>
<data name="buttonFullResync.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="buttonFullResync.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>3, 3, 3, 3</value>
<value>8, 7, 8, 7</value>
</data>
<data name="buttonFullResync.Size" type="System.Drawing.Size, System.Drawing">
<value>242, 29</value>
<value>648, 56</value>
</data>
<data name="buttonFullResync.TabIndex" type="System.Int32, mscorlib">
<value>3</value>
@ -565,13 +595,16 @@
<value>Fill</value>
</data>
<data name="_labelResyncOption.Location" type="System.Drawing.Point, System.Drawing">
<value>86, 219</value>
<value>227, 469</value>
</data>
<data name="_labelResyncOption.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 0, 8, 0</value>
</data>
<data name="_labelResyncOption.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>0, 6, 0, 0</value>
<value>0, 14, 0, 0</value>
</data>
<data name="_labelResyncOption.Size" type="System.Drawing.Size, System.Drawing">
<value>242, 59</value>
<value>648, 223</value>
</data>
<data name="_labelResyncOption.TabIndex" type="System.Int32, mscorlib">
<value>4</value>
@ -592,13 +625,16 @@
<value>Fill</value>
</data>
<data name="_layoutMain.Location" type="System.Drawing.Point, System.Drawing">
<value>3, 3</value>
<value>8, 7</value>
</data>
<data name="_layoutMain.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="_layoutMain.RowCount" type="System.Int32, mscorlib">
<value>8</value>
</data>
<data name="_layoutMain.Size" type="System.Drawing.Size, System.Drawing">
<value>331, 278</value>
<value>883, 692</value>
</data>
<data name="_layoutMain.TabIndex" type="System.Int32, mscorlib">
<value>1</value>
@ -624,11 +660,14 @@
<data name="_layout.Location" type="System.Drawing.Point, System.Drawing">
<value>0, 0</value>
</data>
<data name="_layout.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="_layout.RowCount" type="System.Int32, mscorlib">
<value>2</value>
</data>
<data name="_layout.Size" type="System.Drawing.Size, System.Drawing">
<value>337, 323</value>
<value>899, 770</value>
</data>
<data name="_layout.TabIndex" type="System.Int32, mscorlib">
<value>0</value>
@ -652,13 +691,16 @@
<value>True</value>
</metadata>
<data name="$this.AutoScaleDimensions" type="System.Drawing.SizeF, System.Drawing">
<value>6, 13</value>
<value>16, 31</value>
</data>
<data name="$this.AutoSize" type="System.Boolean, mscorlib">
<value>True</value>
</data>
<data name="$this.ClientSize" type="System.Drawing.Size, System.Drawing">
<value>337, 323</value>
<value>899, 770</value>
</data>
<data name="$this.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>8, 7, 8, 7</value>
</data>
<data name="$this.StartPosition" type="System.Windows.Forms.FormStartPosition, System.Windows.Forms">
<value>CenterParent</value>

View File

@ -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")]

View File

@ -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

View File

@ -40,7 +40,7 @@ namespace Acacia.Stubs
#region UI
OutlookUI OutlookUI { get; }
IWin32Window Window { get; }
ISystemWindow Window { get; }
IExplorer GetActiveExplorer();
#endregion

View File

@ -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; }
}
}

View File

@ -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.
/// </summary>
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
{

View File

@ -67,9 +67,13 @@ namespace Acacia.UI.Outlook
Logger.Instance.Trace(Owner, "Command {0}: Handled", Id);
}
public void Invalidate()
/// <summary>
/// Invalidates the command, triggering a reload of labels and images.
/// </summary>
/// <param name="forceUpdate">If true, the Outlook UI will be updated straight away.</param>
public void Invalidate(bool forceUpdate = false)
{
UI?.InvalidateCommand(this);
UI?.InvalidateCommand(this, forceUpdate);
}
private bool _isEnabled = true;

View File

@ -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)

View File

@ -46,7 +46,7 @@ namespace Acacia.UI.Outlook
if (_isPressed != value)
{
_isPressed = value;
UI?.InvalidateCommand(this);
UI?.InvalidateCommand(this, false);
}
}
}

View File

@ -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;
/// <summary>
/// We use a Threading.Timer here, as the schedule may be modified from any thread
/// </summary>
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);
}
}
/// <summary>
/// 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.
/// </summary>
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)
/// <summary>
/// Optional schedule for all accounts.
/// </summary>
private Schedule _scheduleAll;
/// <summary>
/// Optional schedules per account.
/// </summary>
private readonly Dictionary<ZPushAccount, Schedule> _schedulesAccount = new Dictionary<ZPushAccount, Schedule>();
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;
}
/// <summary>
/// Returns the task for execution.
/// </summary>
/// <param name="schedule">The schedule that triggered the task execution, or null if this is a sync.</param>
/// <param name="account">The account for which the tasks are requested.</param>
/// <returns>The task instance, or null if there is no task to execute in this schedule.</returns>
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));
}
}
/// <summary>
/// Sets a custom schedule for the task. This method only records the schedule, execution is done by ZPushSync.
/// </summary>
/// <param name="account">The specific account to set the schedule for, or null to set it for all accounts</param>
/// <param name="schedule">The schedule. Specify null to clear the schedule and resume the original.</param>
public void SetTaskSchedule(ZPushAccount account, Schedule schedule)
{
if (account == null)
_scheduleAll = schedule;
else
{
if (schedule == null)
_schedulesAccount.Remove(account);
else
_schedulesAccount[account] = schedule;
}
}
/// <summary>
/// Returns any custom task scheddule registered for the account, or all accounts.
/// </summary>
/// <param name="account">The account, or null to retrieve the schedule for all accounts.</param>
/// <returns>The schedule, or null if no schedule is registered</returns>
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<SyncTask> _tasks = new List<SyncTask>();
private readonly List<SyncTaskImpl> _tasks = new List<SyncTaskImpl>();
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
/// <param name="owner">The feature owning the task.</param>
/// <param name="name">The task's name, for logging.</param>
/// <param name="action">The action to execute.</param>
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
/// <param name="owner">The feature owning the task.</param>
/// <param name="name">The task's name, for logging.</param>
/// <param name="action">The action to execute.</param>
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;
}
/// <summary>
/// Sets a custom schedule for the specified task.
/// </summary>
/// <param name="task">The task</param>
/// <param name="account">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.</param>
/// <param name="period">The schedule. Specify null to clear the schedule and resume the original.</param>
/// <returns>The old schedule, or null if there was none.</returns>
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);
}
}
/// <summary>
/// Helper to execute a task. Silently ignores null tasks.
/// </summary>
private void ExecuteTask(AcaciaTask task, bool synchronous)
{
if (task == null)
return;
Tasks.Task(task, synchronous);
}
/// <summary>
/// Timer callback, executes any tasks.
/// </summary>