Skip to content

Commit fde78c1

Browse files
committed
Implemented a "please rate" notice.
In the hopes to get more people to rate on the marketplace or give a GitHub star, in the hope that this will boost visibility of the extension.
1 parent 9379adf commit fde78c1

3 files changed

Lines changed: 160 additions & 26 deletions

File tree

VSDoxyHighlighter/GeneralOptionsPage.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,21 @@ public bool IsEnabledInCommentType(CommentType type)
222222

223223
//----------------
224224

225-
// The value is not editable by the user.
225+
// The version of the configuration format stored in the config file.
226226
[Browsable(false)]
227227
public int Version { get; set; } = (int)ConfigVersions.NoVersionInConfig;
228228

229+
// We show the "please rate" notice after this date, stored in ISO 8601 format.
230+
// By default is it null and we set it the first time the package gets loaded.
231+
[Browsable(false)]
232+
public string RateNoticeDateStr { get; set; } // Must be public for serialization.
233+
234+
public void SaveRateNoticeDate(DateTime newDate)
235+
{
236+
RateNoticeDateStr = newDate.ToString("o");
237+
SaveSettingsToStorage();
238+
}
239+
229240

230241
//----------------
231242
// Flags to enable/disable the main features of the extension
@@ -469,16 +480,22 @@ private static void HandleConversionFromStringError(string valueAsString, Except
469480
InfoBar.ShowMessage(
470481
icon: KnownMonikers.StatusError,
471482
message: "VSDoxyHighlighter: Failed to parse Doxygen commands configuration from string. Backed up configuration and restored defaults.",
472-
actions: new (string, Action)[] {
483+
showCloseButton: true,
484+
actions: new (string uiText, bool asButton, Func<bool> callback)[] {
473485
("Show details",
474-
() => MessageBox.Show(
475-
"VSDoxyHighlighter extension: Failed to convert the configuration of the Doxygen commands (which is stored as a JSON string) to an actual 'List<DoxygenCommandInConfig>'. "
476-
+ "Corrupt settings or maybe a bug in the extension?\n"
477-
+ "Default configuration of commands got restored.\n"
478-
+ $"Original JSON string written to: {backupFilename}.\n\n"
479-
+ $"Exception message from the conversion: {ex}\n\n"
480-
+ $"JSON string that failed to get parsed:\n{valueAsString}",
481-
"VSDoxyHighlighter error", MessageBoxButtons.OK, MessageBoxIcon.Error))}
486+
false,
487+
() => {
488+
MessageBox.Show(
489+
"VSDoxyHighlighter extension: Failed to convert the configuration of the Doxygen commands (which is stored as a JSON string) to an actual 'List<DoxygenCommandInConfig>'. "
490+
+ "Corrupt settings or maybe a bug in the extension?\n"
491+
+ "Default configuration of commands got restored.\n"
492+
+ $"Original JSON string written to: {backupFilename}.\n\n"
493+
+ $"Exception message from the conversion: {ex}\n\n"
494+
+ $"JSON string that failed to get parsed:\n{valueAsString}",
495+
"VSDoxyHighlighter error", MessageBoxButtons.OK, MessageBoxIcon.Error);
496+
return false;
497+
}
498+
)}
482499
);
483500
}
484501
}

VSDoxyHighlighter/InfoBar.cs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ internal static class InfoBar
1515
/// InfoBar.ShowMessage(
1616
/// icon: KnownMonikers.StatusError,
1717
/// message: "test message",
18-
/// actions: new(string, Action)[] {
19-
/// ("yes", () => MessageBox.Show("You clicked Yes!")),
20-
/// ("no", () => MessageBox.Show("You clicked No!"))}
18+
/// actions: new(string uiText, bool asButton, Func<bool /*closeInfoBar*/> callback)[] {
19+
/// ("yes", false, () => { MessageBox.Show("You clicked Yes!"); return false; }),
20+
/// ("no", false, () => { MessageBox.Show("You clicked No!"); return false; }),
21+
/// }
2122
/// );
2223
///
2324
/// Note: Must be called from the main thread!
@@ -27,10 +28,17 @@ internal static class InfoBar
2728
///
2829
/// <param name="icon">Use the members of KnownMonikers. E.g. KnownMonikers.StatusInformation</param>
2930
/// <param name="message">The displayed message.</param>
30-
/// <param name="actions">For each element in the array, a hyperlink with the given string as message gets created.
31-
/// When the user clicks on the hyperlinks, the given action gets executed. Use "null" or an empty array in case
32-
/// you do not need any hyperlinks (or the dedicated overload without an "actions" item.</param>
33-
public static void ShowMessage(ImageMoniker icon, string message, (string, Action)[] actions)
31+
/// <param name="actions">For each element in the array, a hyperlink or button with the given string as message gets created.
32+
/// When the user clicks on one of these, the given callback gets executed. If the callback returns true, the info bar is
33+
/// closed afterwards. If the callback return false, the info bar stays open.
34+
/// Set the actions parameter to "null" or an empty array in case you do not need any interactive elements (or use the
35+
/// dedicated overload without an "actions" parameter).
36+
/// </param>
37+
public static void ShowMessage(
38+
ImageMoniker icon,
39+
string message,
40+
bool showCloseButton,
41+
(string uiText, bool asButton, Func<bool /*closeInfoBar*/> callback)[] actions)
3442
{
3543
ThreadHelper.ThrowIfNotOnUIThread();
3644

@@ -55,17 +63,22 @@ public static void ShowMessage(ImageMoniker icon, string message, (string, Actio
5563
return;
5664
}
5765

58-
List<InfoBarHyperlink> hyperlinks = new List<InfoBarHyperlink>();
66+
var actionItems = new List<IVsInfoBarActionItem>();
5967
if (actions != null) {
6068
foreach (var action in actions) {
61-
hyperlinks.Add(new InfoBarHyperlink(action.Item1, action.Item2));
69+
if (action.asButton) {
70+
actionItems.Add(new InfoBarButton(action.uiText, action.callback));
71+
}
72+
else {
73+
actionItems.Add(new InfoBarHyperlink(action.uiText, action.callback));
74+
}
6275
}
6376
}
6477

65-
InfoBarModel infoBarModel = new InfoBarModel(message, hyperlinks, icon, isCloseButtonVisible: true);
78+
InfoBarModel infoBarModel = new InfoBarModel(message, actionItems, icon, isCloseButtonVisible: showCloseButton);
6679

6780
IVsInfoBarUIElement uiElement = infoBarFactory.CreateInfoBar(infoBarModel);
68-
if (hyperlinks.Count > 0) {
81+
if (actionItems.Count > 0) {
6982
InfoBarEvents events = new InfoBarEvents();
7083
uiElement.Advise(events, out events.mCookie);
7184
}
@@ -79,12 +92,12 @@ public static void ShowMessage(ImageMoniker icon, string message, (string, Actio
7992
public static void ShowMessage(ImageMoniker icon, string message)
8093
{
8194
ThreadHelper.ThrowIfNotOnUIThread();
82-
ShowMessage(icon, message, null);
95+
ShowMessage(icon, message, true, null);
8396
}
8497

8598

8699
/// <summary>
87-
/// Helper class containing the callbacks from the hyperlinks in the info bar message.
100+
/// Helper class containing the callbacks from the actionItems in the info bar message.
88101
/// </summary>
89102
private class InfoBarEvents : IVsInfoBarUIEvents
90103
{
@@ -93,8 +106,10 @@ public InfoBarEvents() { }
93106
public void OnActionItemClicked(IVsInfoBarUIElement infoBarUIElement, IVsInfoBarActionItem actionItem)
94107
{
95108
ThreadHelper.ThrowIfNotOnUIThread();
96-
Action action = (Action)actionItem.ActionContext;
97-
action();
109+
var callback = (Func<bool>)actionItem.ActionContext;
110+
if (callback()) {
111+
infoBarUIElement.Close();
112+
}
98113
}
99114

100115
public void OnClosed(IVsInfoBarUIElement infoBarUIElement)

VSDoxyHighlighter/VSDoxyHighlighterPackage.cs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
using Microsoft.VisualStudio.Shell;
33
using Microsoft.VisualStudio.Shell.Interop;
44
using System;
5+
using System.Diagnostics;
6+
using System.Globalization;
57
using System.Runtime.InteropServices;
68
using System.Threading;
7-
using System.Windows.Forms;
89
using Task = System.Threading.Tasks.Task;
910

1011
namespace VSDoxyHighlighter
@@ -70,9 +71,12 @@ public static CommentParser CommentParser {
7071
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
7172
{
7273
await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
74+
7375
mGeneralOptions = (GeneralOptionsPage)GetDialogPage(typeof(GeneralOptionsPage));
7476
mDoxygenCommands = new DoxygenCommands(mGeneralOptions);
7577
mCommentParser = new CommentParser(mDoxygenCommands);
78+
79+
HandleRateNoticeAfterLoad();
7680
}
7781

7882
protected override void Dispose(bool disposing)
@@ -84,6 +88,7 @@ protected override void Dispose(bool disposing)
8488
base.Dispose(disposing);
8589
}
8690

91+
8792
// Loads our package, especially getting the options pages.
8893
private static void LoadPackage()
8994
{
@@ -116,6 +121,103 @@ private static void LoadPackage()
116121
}
117122

118123

124+
enum RateNoticeAction
125+
{
126+
None,
127+
Initialize,
128+
Show,
129+
}
130+
131+
132+
private void HandleRateNoticeAfterLoad()
133+
{
134+
ThreadHelper.ThrowIfNotOnUIThread();
135+
136+
switch (GetRateNoticeAction()) {
137+
case RateNoticeAction.Initialize:
138+
// 30 days after the extension got loaded for the first time seems like a reasonable date that gave
139+
// users enough time to try out the extension.
140+
mGeneralOptions.SaveRateNoticeDate(DateTime.Now.AddDays(30));
141+
break;
142+
case RateNoticeAction.Show:
143+
ShowRateNoticeNow();
144+
break;
145+
}
146+
}
147+
148+
149+
private RateNoticeAction GetRateNoticeAction()
150+
{
151+
if (string.IsNullOrEmpty(mGeneralOptions.RateNoticeDateStr)) {
152+
return RateNoticeAction.Initialize;
153+
}
154+
155+
if (DateTime.TryParseExact(mGeneralOptions.RateNoticeDateStr, "o", CultureInfo.InvariantCulture,
156+
DateTimeStyles.RoundtripKind, out DateTime rateNoticeDate)) {
157+
if (DateTime.Now >= rateNoticeDate) {
158+
return RateNoticeAction.Show;
159+
}
160+
else {
161+
return RateNoticeAction.None;
162+
}
163+
}
164+
else {
165+
ActivityLog.LogError("VSDoxyHighlighter", $"Failed to parse RateNoticeDateStr '{mGeneralOptions.RateNoticeDateStr}'. Resetting...");
166+
return RateNoticeAction.Initialize;
167+
}
168+
}
169+
170+
171+
private void ShowRateNoticeNow()
172+
{
173+
ThreadHelper.ThrowIfNotOnUIThread();
174+
175+
InfoBar.ShowMessage(
176+
icon: KnownMonikers.StatusInformation,
177+
message: "You are using the VSDoxyHighlighter extension which provides syntax highlighting, IntelliSense and " +
178+
"quick infos for doxygen/javadoc style comments. If you find it useful, please consider giving the " +
179+
"project a star on GitHub and leaving a rating on the Visual Studio Marketplace.",
180+
showCloseButton: false,
181+
actions: new (string uiText, bool asButton, Func<bool /*closeInfoBar*/> callback)[] {
182+
(
183+
uiText: "Open GitHub page",
184+
asButton: false,
185+
callback: () => {
186+
Process.Start(new ProcessStartInfo("https://github.com/Sedeniono/VSDoxyHighlighter") { UseShellExecute = true });
187+
return false;
188+
}
189+
),
190+
(
191+
uiText: "Open Marketplace",
192+
asButton: false,
193+
callback: () => {
194+
Process.Start(new ProcessStartInfo("https://marketplace.visualstudio.com/items?itemName=Sedenion.VSDoxyHighlighter") { UseShellExecute = true });
195+
return false;
196+
}
197+
),
198+
(
199+
uiText: "Remind me in 5 days",
200+
asButton: true,
201+
callback: () => {
202+
// We don't choose 7 days and instead some "uneven" number of days and hours to prevent
203+
// the notice from appearing again at the same time of day and the same day of the week,
204+
// in case it originally interrupted a daily or weekly meeting.
205+
mGeneralOptions.SaveRateNoticeDate(DateTime.Now.AddDays(5).AddHours(4));
206+
return true;
207+
}
208+
),
209+
(
210+
uiText: "Never show this again",
211+
asButton: true,
212+
callback: () => {
213+
mGeneralOptions.SaveRateNoticeDate(DateTime.MaxValue);
214+
return true;
215+
}
216+
)
217+
});
218+
}
219+
220+
119221
private const string PackageGuidString = "72c95c0a-4101-4d94-a4e0-5be5e28bdf02";
120222
private static Guid PackageGuid = new Guid(PackageGuidString);
121223

0 commit comments

Comments
 (0)