Add daily tips to start screen

The initial screen is largely blank, with just the four large buttons
for new/open/recent1/recent2.  It now also has a "tip of the day" box,
with text and an optional image.

The tips and images are kept in the RuntimeData directory.  They're
small enough that they could have been baked into the binary, but
there's enough other stuff going on there that it didn't seem
necessary.  Also, if the tips annoy you, removing the tips file will
hide the tip UI.

The index of the tip shown is based on the day of the year, modulo
the number of defined tips.  So it will be different every day (with
a bit of hand-waving at the end of the year).
This commit is contained in:
Andy McFadden 2021-10-15 15:36:56 -07:00
parent 2008558870
commit adf5726f62
12 changed files with 377 additions and 8 deletions

View File

@ -1716,8 +1716,8 @@ namespace CommonUtil {
// Fixed region follows.
Test_Expect(AddResult.Okay, ref result, map.AddEntry(0x000200, 0x0100, 0x3000));
string mapStr = map.FormatAddressMap(); // DEBUG - format the map and
Debug.WriteLine(mapStr); // DEBUG - print it to the console
//string mapStr = map.FormatAddressMap(); // DEBUG - format the map and
//Debug.WriteLine(mapStr); // DEBUG - print it to the console
// Add a region that starts in the middle of the floating region (becoming
// a sibling), and ends after the fixed region (becoming its parent).

179
SourceGen/DailyTips.cs Normal file
View File

@ -0,0 +1,179 @@
/*
* Copyright 2021 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Web.Script.Serialization;
using System.Windows.Media.Imaging;
namespace SourceGen {
/// <summary>
/// Holds the collection of daily tips and associated images.
/// </summary>
public class DailyTips {
private static string TIPS_FILE = "daily-tips.json";
private static string TIPS_DIR = "Tips";
/// <summary>
/// A tip is a text string with an optional bitmap.
/// </summary>
public class Tip {
public string Text { get; private set; }
public string ImageFileName { get; private set; }
public BitmapSource Bitmap { get; }
public Tip(string text, string imageFileName) {
Text = text;
ImageFileName = imageFileName;
if (!string.IsNullOrEmpty(imageFileName)) {
Bitmap = LoadImage(imageFileName);
}
}
private static BitmapSource LoadImage(string fileName) {
string pathName = RuntimeDataAccess.GetPathName(TIPS_DIR);
pathName = Path.Combine(pathName, fileName);
try {
BitmapImage bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(pathName);
bitmap.CacheOption = BitmapCacheOption.OnLoad; // don't hold file open
bitmap.EndInit();
return bitmap;
} catch (Exception ex) {
Debug.WriteLine("Unable to load bitmap '" + fileName + "': " + ex);
return null;
}
}
private static BitmapSource LoadImage1(string fileName) {
string pathName = RuntimeDataAccess.GetPathName(TIPS_DIR);
pathName = Path.Combine(pathName, fileName);
try {
// From "How to: Encode and Decode a PNG Image".
// Note: this holds the file open (try deleting the file).
BitmapSource bitmapSource;
Stream imageStreamSource = new FileStream(pathName, FileMode.Open,
FileAccess.Read, FileShare.Read);
PngBitmapDecoder decoder = new PngBitmapDecoder(imageStreamSource,
BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
bitmapSource = decoder.Frames[0];
//imageStreamSource.Dispose(); // image becomes blank
return bitmapSource;
} catch (IOException ex) {
Debug.WriteLine("Unable to load image '" + fileName + "': " + ex);
return null;
}
}
}
/// <summary>
/// List of tips.
/// </summary>
private List<Tip> mTips = new List<Tip>();
public DailyTips() { }
/// <summary>
/// Loads the list of tips. The list will always have at least one element.
/// </summary>
public bool Load() {
mTips.Clear();
if (!DoLoadTips()) {
mTips.Add(new Tip(Res.Strings.TIPS_NOT_AVAILABLE, null));
return false;
} else {
Debug.WriteLine("Loaded " + mTips.Count + " tips");
return true;
}
}
public int DailyNumber {
get {
// We show a different tip every day by taking the day-of-year value and
// modding it by the number of tips we have. Doesn't do the right thing
// at the end of year transition, but everybody is off partying anyway.
if (mTips.Count > 0) {
int doy = DateTime.Now.DayOfYear;
return doy % mTips.Count;
} else {
return 0;
}
}
}
public int Count { get { return mTips.Count; } }
/// <summary>
/// Returns the Nth tip.
/// </summary>
public Tip Get(int index) {
if (mTips.Count == 0) {
// Load tips, or at least generate a "not available" entry.
Load();
}
if (index < 0 || index >= mTips.Count) {
Debug.WriteLine("Invalid request for tip " + index);
return mTips[0];
}
return mTips[index];
}
[Serializable]
internal class SerTip {
public string Text { get; set; }
public string Image { get; set; }
}
internal class SerTipFile {
public List<SerTip> Tips { get; set; }
}
private bool DoLoadTips() {
string pathName = RuntimeDataAccess.GetPathName(TIPS_DIR);
pathName = Path.Combine(pathName, TIPS_FILE);
string cereal;
try {
cereal = File.ReadAllText(pathName);
} catch (IOException ex) {
Debug.WriteLine("Failed reading tip file '" + pathName + "': " + ex.Message);
return false;
}
JavaScriptSerializer ser = new JavaScriptSerializer();
SerTipFile tipFile;
try {
tipFile = ser.Deserialize<SerTipFile>(cereal);
} catch (Exception ex) {
Debug.WriteLine("Failed deserializing tip JSON: " + ex.Message);
return false;
}
if (tipFile == null) {
Debug.WriteLine("Failed to find tip list");
return false;
}
foreach (SerTip serTip in tipFile.Tips) {
mTips.Add(new Tip(serTip.Text, serTip.Image));
}
return true;
}
}
}

View File

@ -193,6 +193,8 @@ limitations under the License.
<system:String x:Key="str_SymbolImportCaption">Symbol Import</system:String>
<system:String x:Key="str_SymbolImportGoodFmt">Imported {0} global symbols.</system:String>
<system:String x:Key="str_SymbolImportNone">No global+export symbols were found.</system:String>
<system:String x:Key="str_TipsLoading">Loading tips...</system:String>
<system:String x:Key="str_TipsNotAvailable">Daily tips are not available.</system:String>
<system:String x:Key="str_TitleBase">6502bench SourceGen</system:String>
<system:String x:Key="str_TitleModified">(save needed)</system:String>
<system:String x:Key="str_TitleNewProject">[new project]</system:String>

View File

@ -367,6 +367,10 @@ namespace SourceGen.Res {
(string)Application.Current.FindResource("str_SymbolImportGoodFmt");
public static string SYMBOL_IMPORT_NONE =
(string)Application.Current.FindResource("str_SymbolImportNone");
public static string TIPS_LOADING =
(string)Application.Current.FindResource("str_TipsLoading");
public static string TIPS_NOT_AVAILABLE =
(string)Application.Current.FindResource("str_TipsNotAvailable");
public static string TITLE_BASE =
(string)Application.Current.FindResource("str_TitleBase");
public static string TITLE_MODIFIED =

View File

@ -0,0 +1,52 @@
{
"_copyright" : "Copyright 2021 faddenSoft. All rights reserved.",
"_license" : "Apache 2.0; see LICENSE.txt for details",
"Tips" : [
{
"Text" : "Many disassemblers assume everything is code, and ask you to separate out the data. SourceGen automatically finds all reachable code, so you just need to identify the places where the code starts."
},
{
"Text" : "Data that follows a JSR or JSL should be marked as \"inline data\". This allows the code analyzer to skip over it. Common situations, such as null-terminated strings and addresses, can be handled automatically.",
"Image" : "print-inline-sample.png"
},
{
"Text" : "Most screen elements will respond to a double-click, either by jumping to a symbol or opening an appropriate editor. For example, you can double-click in the Bytes column to see a hex dump at that address."
},
{
"Text" : "You can jump to the address referred to by an instruction operand by selecting the line and hitting Ctrl+J, or simply by double-clicking on the opcode.",
"Image" : "dbl-click-opcode.png"
},
{
"Text" : "You can configure SourceGen to look more like your favorite assembler. The Application Settings editor lets you configure pseudo-op directives, upper/lower case, and much more.",
"Image" : "pseudo-op-names.png"
},
{
"Text" : "The References window shows all locations that reference the selected line. Double-click on an entry to jump directly there."
},
{
"Text" : "Use the Goto feature (Ctrl+G) to jump to an address, file offset, or label."
},
{
"Text" : "All actions that affect the project are added to the undo/redo buffer. Feel free to experiment."
},
{
"Text" : "Notes are like full-line comments, but they don't appear in generated source code, so you can use them to make notes while you work. They also serve as color-coded bookmarks.",
"Image" : "note-sample.png"
},
{
"Text" : "You're not limited to global labels. You can create non-unique local labels, like \"@LOOP\", and define multiple labels for zero-page addresses in local variable tables."
},
{
"Text" : "2D bitmap images and 3D wireframe meshes can be converted to images that are displayed inline. This can make it much easier to figure out what a piece of code is drawing."
},
{
"Text" : "Large address tables can be formatted with a single operation. Various arrangements of address bytes are supported."
},
{
"Text" : "Source code can be generated for several cross-assemblers, or exported to HTML with embedded graphics. Animations can be exported as animated GIFs."
},
{
"Text" : "The online tutorial at 6502bench.com has many more tips."
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -40,6 +40,7 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
@ -75,6 +76,7 @@
<Compile Include="AsmGen\IAssembler.cs" />
<Compile Include="AsmGen\IGenerator.cs" />
<Compile Include="AsmGen\LabelLocalizer.cs" />
<Compile Include="DailyTips.cs" />
<Compile Include="Exporter.cs" />
<Compile Include="FormattedOperandCache.cs" />
<Compile Include="LocalVariableLookup.cs" />

View File

@ -640,12 +640,16 @@ limitations under the License.
<!-- Launch panel. Either this or the code list panel will be visible. -->
<Grid Grid.Column="2" Name="launchPanel" Visibility="{Binding Path=LaunchPanelVisibility}"
d:IsHidden="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<StackPanel Grid.Row="0" Grid.ColumnSpan="2" Orientation="Horizontal">
<Image Source="/SourceGen;component/Res/Logo.png" Height="100"/>
<Grid Margin="8">
<Grid.RowDefinitions>
@ -662,13 +666,14 @@ limitations under the License.
</Grid>
</StackPanel>
<StackPanel Grid.Row="1" HorizontalAlignment="Left">
<!-- recents -->
<StackPanel Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left">
<Button Content="Start new project" Width="240" Height="50" Margin="10,30,10,10"
Command="{StaticResource NewProjectCmd}"/>
<Button Content="Open existing project" Width="240" Height="50" Margin="10"
Command="{StaticResource OpenCmd}"/>
<Button Width="240" Height="50" Margin="10" Visibility="{Binding RecentProjectVisibility1}"
ToolTip="{Binding RecentProjectPath1}" ToolTipService.InitialShowDelay="1000"
ToolTip="{Binding RecentProjectPath1}" ToolTipService.InitialShowDelay="750"
Command="{DynamicResource RecentProjectCmd}" CommandParameter="0">
<Button.Content>
<StackPanel>
@ -678,7 +683,7 @@ limitations under the License.
</Button.Content>
</Button>
<Button Width="240" Height="50" Margin="10" Visibility="{Binding RecentProjectVisibility2}"
ToolTip="{Binding RecentProjectPath2}" ToolTipService.InitialShowDelay="1000"
ToolTip="{Binding RecentProjectPath2}" ToolTipService.InitialShowDelay="750"
Command="{DynamicResource RecentProjectCmd}" CommandParameter="1">
<Button.Content>
<StackPanel>
@ -688,10 +693,49 @@ limitations under the License.
</Button.Content>
</Button>
</StackPanel>
<!-- daily tips -->
<GroupBox Grid.Column="1" Grid.Row="1" Width="300" MinHeight="210" Padding="2,0" Margin="20,20,0,0"
VerticalAlignment="Top" Visibility="{Binding DailyTipVisibility}"
Header="Tip of the Day">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap" VerticalAlignment="Center"
Text="{Binding DailyTipText, FallbackValue=Tip text here!}"/>
<Image Grid.Row="1" Margin="0,4,0,0"
Source="{Binding DailyTipImage}"
RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="None"/>
<Grid Grid.Row="2" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Width="70" HorizontalAlignment="Left"
Content="Previous"
IsEnabled="{Binding IsEnabledDailyTipPrev}"
Click="DailyTipPrevious_Click"/>
<TextBlock Grid.Column="1" HorizontalAlignment="Center" Margin="0,2,0,0"
Text="{Binding DailyTipPosStr, FallbackValue=?/?}"/>
<Button Grid.Column="2" Width="70" HorizontalAlignment="Right"
Content="Next"
IsEnabled="{Binding IsEnabledDailyTipNext}"
Click="DailyTipNext_Click"/>
</Grid>
</Grid>
</GroupBox>
</Grid>
<!-- Code list panel. Either this or the Launch panel will be visible. -->
<Grid Grid.Column="2" Name="codeListGrid" Visibility="{Binding Path=CodeListVisibility}">
<Grid Grid.Column="2" Name="codeListGrid" Visibility="{Binding Path=CodeListVisibility}"
d:IsHidden="False">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>

View File

@ -28,6 +28,7 @@ using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CommonUtil;
using CommonWPF;
@ -133,6 +134,9 @@ namespace SourceGen.WpfGui {
private ResourceDictionary mLightTheme;
private ResourceDictionary mDarkTheme;
// Daily tips.
private DailyTips mDailyTips;
public MainWindow() {
Debug.WriteLine("START at " + DateTime.Now.ToLocalTime());
@ -360,6 +364,8 @@ namespace SourceGen.WpfGui {
mMainCtrl.WindowLoaded();
CreateCodeListContextMenu();
InitDailyTip();
#if DEBUG
// Get more info on CollectionChanged events that do not agree with current
// state of Items collection.
@ -1645,7 +1651,6 @@ namespace SourceGen.WpfGui {
#endregion Misc
#region References panel
public class ReferencesListItem {
@ -2149,5 +2154,86 @@ namespace SourceGen.WpfGui {
}
#endregion Message list panel
#region Daily tips
private int mDailyTipCurrent = 0;
private int mDailyTipMax = 0;
public Visibility DailyTipVisibility {
get { return mDailyTipVisibility; }
set { mDailyTipVisibility = value; OnPropertyChanged(); }
}
private Visibility mDailyTipVisibility = Visibility.Collapsed;
public string DailyTipText {
get {
if (mDailyTipText == null) {
return Res.Strings.TIPS_LOADING;
}
return mDailyTipText;
}
set { mDailyTipText = value; OnPropertyChanged(); }
}
private string mDailyTipText;
public BitmapSource DailyTipImage {
get { return mDailyTipImage; }
set { mDailyTipImage = value; OnPropertyChanged(); }
}
private BitmapSource mDailyTipImage;
public bool IsEnabledDailyTipPrev {
get { return mIsEnabledDailyTipPrev; }
set { mIsEnabledDailyTipPrev = value; OnPropertyChanged(); }
}
private bool mIsEnabledDailyTipPrev;
public bool IsEnabledDailyTipNext {
get { return mIsEnabledDailyTipNext; }
set { mIsEnabledDailyTipNext = value; OnPropertyChanged(); }
}
private bool mIsEnabledDailyTipNext;
public string DailyTipPosStr {
get { return mDailyTipPosStr; }
set { mDailyTipPosStr = value; OnPropertyChanged(); }
}
private string mDailyTipPosStr;
private void DailyTipPrevious_Click(object sender, RoutedEventArgs e) {
if (mDailyTipCurrent > 0) {
mDailyTipCurrent--;
UpdateDailyTip();
}
}
private void DailyTipNext_Click(object sender, RoutedEventArgs e) {
if (mDailyTipCurrent < mDailyTipMax - 1) {
mDailyTipCurrent++;
UpdateDailyTip();
}
}
public void InitDailyTip() {
mDailyTips = new DailyTips();
if (mDailyTips.Load()) {
DailyTipVisibility = Visibility.Visible;
}
mDailyTipCurrent = mDailyTips.DailyNumber;
mDailyTipMax = mDailyTips.Count;
UpdateDailyTip();
}
private void UpdateDailyTip() {
DailyTips.Tip tip = mDailyTips.Get(mDailyTipCurrent);
DailyTipText = tip.Text;
DailyTipImage = tip.Bitmap;
IsEnabledDailyTipPrev = (mDailyTipCurrent > 0);
IsEnabledDailyTipNext = (mDailyTipCurrent < mDailyTipMax - 1);
DailyTipPosStr = string.Format("{0}/{1}", mDailyTipCurrent + 1, mDailyTipMax);
}
#endregion Daily tips
}
}