mirror of
https://github.com/greenshot/greenshot
synced 2025-08-22 06:23:24 -07:00
Use SixLabor.Fonts for rendering
This commit is contained in:
parent
fde1539ac5
commit
69fcb22df2
11 changed files with 5536 additions and 20 deletions
23
src/Greenshot.Editor/Controls/EmojiControl.cs
Normal file
23
src/Greenshot.Editor/Controls/EmojiControl.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using Greenshot.Editor.Drawing;
|
||||||
|
|
||||||
|
namespace Greenshot.Editor.Controls
|
||||||
|
{
|
||||||
|
internal class EmojiControl : Image
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty EmojiProperty = DependencyProperty.Register("Emoji", typeof(string), typeof(EmojiControl), new PropertyMetadata(default(string), PropertyChangedCallback));
|
||||||
|
|
||||||
|
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
((EmojiControl)d).Source = EmojiRenderer.GetBitmapSource((string)e.NewValue, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Emoji
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(EmojiProperty); }
|
||||||
|
set { SetValue(EmojiProperty, value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
281
src/Greenshot.Editor/Controls/EmojiData.cs
Normal file
281
src/Greenshot.Editor/Controls/EmojiData.cs
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
//
|
||||||
|
// Emoji.Wpf — Emoji support for WPF
|
||||||
|
//
|
||||||
|
// Copyright © 2017—2021 Sam Hocevar <sam@hocevar.net>
|
||||||
|
//
|
||||||
|
// This library is free software. It comes without any warranty, to
|
||||||
|
// the extent permitted by applicable law. You can redistribute it
|
||||||
|
// and/or modify it under the terms of the Do What the Fuck You Want
|
||||||
|
// to Public License, Version 2, as published by the WTFPL Task Force.
|
||||||
|
// See http://www.wtfpl.net/ for more details.
|
||||||
|
//
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Greenshot.Editor.Controls
|
||||||
|
{
|
||||||
|
public static class EmojiData
|
||||||
|
{
|
||||||
|
public static IEnumerable<Emoji> AllEmoji
|
||||||
|
=> from g in AllGroups
|
||||||
|
from e in g.EmojiList
|
||||||
|
select e;
|
||||||
|
|
||||||
|
public static IList<Group> AllGroups { get; private set; }
|
||||||
|
|
||||||
|
public static IDictionary<string, Emoji> LookupByText { get; private set; }
|
||||||
|
= new Dictionary<string, Emoji>();
|
||||||
|
public static IDictionary<string, Emoji> LookupByName { get; private set; }
|
||||||
|
= new Dictionary<string, Emoji>();
|
||||||
|
|
||||||
|
public static Regex MatchOne { get; private set; }
|
||||||
|
public static HashSet<char> MatchStart { get; private set; }
|
||||||
|
= new HashSet<char>();
|
||||||
|
|
||||||
|
// FIXME: should we lazy load this? If the user calls Load() later, then
|
||||||
|
// this first Load() call will have been for nothing.
|
||||||
|
static EmojiData() => Load();
|
||||||
|
|
||||||
|
public static void Load()
|
||||||
|
{
|
||||||
|
ParseEmojiList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Emoji
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Text { get; set; }
|
||||||
|
public bool HasVariations => VariationList.Count > 0;
|
||||||
|
|
||||||
|
public Group Group => SubGroup.Group;
|
||||||
|
public SubGroup SubGroup;
|
||||||
|
|
||||||
|
public IList<Emoji> VariationList { get; } = new List<Emoji>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubGroup
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public Group Group;
|
||||||
|
|
||||||
|
public IList<Emoji> EmojiList { get; } = new List<Emoji>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Group
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Icon => SubGroups.FirstOrDefault()?.EmojiList.FirstOrDefault()?.Text;
|
||||||
|
|
||||||
|
public IList<SubGroup> SubGroups { get; } = new List<SubGroup>();
|
||||||
|
|
||||||
|
public int EmojiCount
|
||||||
|
=> SubGroups.Select(s => s.EmojiList.Count).Sum();
|
||||||
|
|
||||||
|
public IEnumerable<IEnumerable<Emoji>> EmojiChunkList
|
||||||
|
=> new ChunkHelper<Emoji>(EmojiList, 8);
|
||||||
|
|
||||||
|
public IEnumerable<Emoji> EmojiList
|
||||||
|
=> from s in SubGroups
|
||||||
|
from e in s.EmojiList
|
||||||
|
select e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string m_match_one_string;
|
||||||
|
|
||||||
|
// FIXME: this could be read directly from emoji-test.txt.gz
|
||||||
|
private static List<string> SkinToneComponents = new List<string>
|
||||||
|
{
|
||||||
|
"🏻", // light skin tone
|
||||||
|
"🏼", // medium-light skin tone
|
||||||
|
"🏽", // medium skin tone
|
||||||
|
"🏾", // medium-dark skin tone
|
||||||
|
"🏿", // dark skin tone
|
||||||
|
};
|
||||||
|
|
||||||
|
private static List<string> HairStyleComponents = new List<string>
|
||||||
|
{
|
||||||
|
"🦰", // red hair
|
||||||
|
"🦱", // curly hair
|
||||||
|
"🦳", // white hair
|
||||||
|
"🦲", // bald
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ToColonSyntax(string s)
|
||||||
|
=> Regex.Replace(s.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-");
|
||||||
|
|
||||||
|
private static void ParseEmojiList()
|
||||||
|
{
|
||||||
|
var match_group = new Regex(@"^# group: (.*)");
|
||||||
|
var match_subgroup = new Regex(@"^# subgroup: (.*)");
|
||||||
|
var match_sequence = new Regex(@"^([0-9a-fA-F ]+[0-9a-fA-F]).*; *([-a-z]*) *# [^ ]* (E[0-9.]* )?(.*)");
|
||||||
|
var match_skin_tone = new Regex($"({string.Join("|", SkinToneComponents)})");
|
||||||
|
var match_hair_style = new Regex($"({string.Join("|", HairStyleComponents)})");
|
||||||
|
|
||||||
|
var adult = "(👨|👩)(🏻|🏼|🏽|🏾|🏿)?";
|
||||||
|
var child = "(👦|👧|👶)(🏻|🏼|🏽|🏾|🏿)?";
|
||||||
|
var match_family = new Regex($"{adult}(\u200d{adult})*(\u200d{child})+");
|
||||||
|
|
||||||
|
var qualified_lut = new Dictionary<string, string>();
|
||||||
|
var list = new List<Group>();
|
||||||
|
var alltext = new List<string>();
|
||||||
|
|
||||||
|
Group current_group = null;
|
||||||
|
SubGroup current_subgroup = null;
|
||||||
|
|
||||||
|
foreach (var line in EmojiDescriptionLines())
|
||||||
|
{
|
||||||
|
var m = match_group.Match(line);
|
||||||
|
if (m.Success)
|
||||||
|
{
|
||||||
|
current_group = new Group { Name = m.Groups[1].ToString() };
|
||||||
|
list.Add(current_group);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
m = match_subgroup.Match(line);
|
||||||
|
if (m.Success)
|
||||||
|
{
|
||||||
|
current_subgroup = new SubGroup { Name = m.Groups[1].ToString(), Group = current_group };
|
||||||
|
current_group.SubGroups.Add(current_subgroup);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
m = match_sequence.Match(line);
|
||||||
|
if (m.Success)
|
||||||
|
{
|
||||||
|
string sequence = m.Groups[1].ToString();
|
||||||
|
string name = m.Groups[4].ToString();
|
||||||
|
|
||||||
|
string text = string.Join("", from n in sequence.Split(' ')
|
||||||
|
select char.ConvertFromUtf32(Convert.ToInt32(n, 16)));
|
||||||
|
bool has_modifier = false;
|
||||||
|
|
||||||
|
if (match_family.Match(text).Success)
|
||||||
|
{
|
||||||
|
// If this is a family emoji, no need to add it to our big matching
|
||||||
|
// regex, since the match_family regex is already included.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Construct a regex to replace e.g. "🏻" with "(🏻|🏼|🏽|🏾|🏿)" in a big
|
||||||
|
// regex so that we can match all variations of this Emoji even if they are
|
||||||
|
// not in the standard.
|
||||||
|
bool has_nonfirst_modifier = false;
|
||||||
|
var regex_text = match_skin_tone.Replace(
|
||||||
|
match_hair_style.Replace(text, (x) =>
|
||||||
|
{
|
||||||
|
has_modifier = true;
|
||||||
|
has_nonfirst_modifier |= x.Value != HairStyleComponents[0];
|
||||||
|
return match_hair_style.ToString();
|
||||||
|
}), (x) =>
|
||||||
|
{
|
||||||
|
has_modifier = true;
|
||||||
|
has_nonfirst_modifier |= x.Value != SkinToneComponents[0];
|
||||||
|
return match_skin_tone.ToString();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!has_nonfirst_modifier)
|
||||||
|
alltext.Add(has_modifier ? regex_text : text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is already a differently-qualified version of this character, skip it.
|
||||||
|
// FIXME: this only works well if fully-qualified appears first in the list.
|
||||||
|
var unqualified = text.Replace("\ufe0f", "");
|
||||||
|
if (qualified_lut.ContainsKey(unqualified))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
qualified_lut[unqualified] = text;
|
||||||
|
|
||||||
|
var emoji = new Emoji
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Text = text,
|
||||||
|
SubGroup = current_subgroup,
|
||||||
|
};
|
||||||
|
// FIXME: this prevents LookupByText from working with the unqualified version
|
||||||
|
LookupByText[text] = emoji;
|
||||||
|
LookupByName[ToColonSyntax(name)] = emoji;
|
||||||
|
MatchStart.Add(text[0]);
|
||||||
|
|
||||||
|
// Get the left part of the name and check whether we’re a variation of an existing
|
||||||
|
// emoji. If so, append to that emoji. Otherwise, add to current subgroup.
|
||||||
|
// FIXME: does not work properly because variations can appear before the generic emoji
|
||||||
|
if (name.Contains(":") && LookupByName.TryGetValue(ToColonSyntax(name.Split(':')[0]), out var parent_emoji))
|
||||||
|
{
|
||||||
|
if (parent_emoji.VariationList.Count == 0)
|
||||||
|
parent_emoji.VariationList.Add(parent_emoji);
|
||||||
|
parent_emoji.VariationList.Add(emoji);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
current_subgroup.EmojiList.Add(emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the Component group. Not sure we want to have the skin tones in the picker.
|
||||||
|
list.RemoveAll(g => g.Name == "Component");
|
||||||
|
AllGroups = list;
|
||||||
|
|
||||||
|
// Make U+fe0f optional in the regex so that we can match any combination.
|
||||||
|
// FIXME: this is the starting point to implement variation selectors.
|
||||||
|
var sortedtext = alltext.OrderByDescending(x => x.Length);
|
||||||
|
var match_other = string.Join("|", sortedtext)
|
||||||
|
.Replace("*", "[*]")
|
||||||
|
.Replace("\ufe0f", "\ufe0f?");
|
||||||
|
|
||||||
|
// Build a regex that matches any Emoji
|
||||||
|
m_match_one_string = match_family.ToString() + "|" + match_other;
|
||||||
|
MatchOne = new Regex("(" + m_match_one_string + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> EmojiDescriptionLines()
|
||||||
|
{
|
||||||
|
using var stream = Assembly.GetCallingAssembly().GetManifestResourceStream("Greenshot.Editor.Resources.emoji-test.txt");
|
||||||
|
using var streamReader = new StreamReader(stream);
|
||||||
|
return streamReader.ReadToEnd().Split('\r', '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ChunkHelper<T> : IEnumerable<IEnumerable<T>>
|
||||||
|
{
|
||||||
|
public ChunkHelper(IEnumerable<T> elements, int size)
|
||||||
|
{
|
||||||
|
m_elements = elements;
|
||||||
|
m_size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<IEnumerable<T>> GetEnumerator()
|
||||||
|
{
|
||||||
|
using (var enumerator = m_elements.GetEnumerator())
|
||||||
|
{
|
||||||
|
m_has_more = enumerator.MoveNext();
|
||||||
|
while (m_has_more)
|
||||||
|
yield return GetNextBatch(enumerator).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<T> GetNextBatch(IEnumerator<T> enumerator)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_size; ++i)
|
||||||
|
{
|
||||||
|
yield return enumerator.Current;
|
||||||
|
m_has_more = enumerator.MoveNext();
|
||||||
|
if (!m_has_more)
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> GetEnumerator();
|
||||||
|
|
||||||
|
private readonly IEnumerable<T> m_elements;
|
||||||
|
private readonly int m_size;
|
||||||
|
private bool m_has_more;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
129
src/Greenshot.Editor/Controls/EmojiPicker.xaml
Normal file
129
src/Greenshot.Editor/Controls/EmojiPicker.xaml
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<StackPanel x:Class="Greenshot.Editor.Controls.EmojiPicker"
|
||||||
|
x:Name="StackPanel_INTERNAL"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:controls="clr-namespace:Greenshot.Editor.Controls"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<StackPanel.Resources>
|
||||||
|
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||||
|
|
||||||
|
<ControlTemplate x:Key="VariationPopupTemplate" TargetType="ContentControl">
|
||||||
|
<ListView Name="VariationListView" ItemsSource="{Binding VariationList}"
|
||||||
|
BorderThickness="1" Height="Auto" Padding="0" Margin="1"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
|
MaxWidth="280"> <!-- 6 columns -->
|
||||||
|
<ListView.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListView.ItemsPanel>
|
||||||
|
<ListView.ItemContainerStyle>
|
||||||
|
<Style TargetType="{x:Type ListViewItem}">
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="Margin" Value="0"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
</Style>
|
||||||
|
</ListView.ItemContainerStyle>
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button Background="Transparent" BorderBrush="Transparent"
|
||||||
|
Margin="0" Click="OnEmojiPicked"
|
||||||
|
Width="40" Height="40" ToolTip="{Binding Path=Name}">
|
||||||
|
<controls:EmojiControl Height="24" Emoji="{Binding Path=Text, IsAsync=True}"/>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</ControlTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="CellTemplate">
|
||||||
|
<ToggleButton Width="40" Height="40" Margin="0" Padding="2"
|
||||||
|
x:Name="VariationButton" Background="Transparent" BorderBrush="Transparent"
|
||||||
|
Click="OnEmojiPicked" Focusable="False" ToolTip="{Binding Path=Name}">
|
||||||
|
<Grid>
|
||||||
|
<controls:EmojiControl Height="24" Margin="4" Emoji="{Binding Path=Text, IsAsync=True}"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||||
|
<Polygon Visibility="{Binding HasVariations, Converter={StaticResource BoolToVis}}"
|
||||||
|
Width="6" Height="6" VerticalAlignment="Bottom" HorizontalAlignment="Right"
|
||||||
|
Stretch="Fill" Points="0,1 1,1 1,0" Fill="Black"/>
|
||||||
|
<Popup x:Name="VariationPopup" StaysOpen="False" AllowsTransparency="True" Margin="0"
|
||||||
|
IsOpen="{Binding IsChecked, ElementName=VariationButton, Mode=TwoWay}">
|
||||||
|
<ContentControl Template="{StaticResource ResourceKey=VariationPopupTemplate}"/>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
</ToggleButton>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
</StackPanel.Resources>
|
||||||
|
|
||||||
|
<ToggleButton x:Name="Button_INTERNAL">
|
||||||
|
<controls:EmojiControl x:Name="Image" Emoji="{Binding ElementName=StackPanel_INTERNAL, Mode=OneWay, Path=Selection, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
</ToggleButton>
|
||||||
|
|
||||||
|
<Popup IsOpen="{Binding IsChecked, ElementName=Button_INTERNAL, Mode=TwoWay}"
|
||||||
|
StaysOpen="False"
|
||||||
|
KeyDown="OnPopupKeyDown" Loaded="OnPopupLoaded"
|
||||||
|
AllowsTransparency="True" Margin="0">
|
||||||
|
|
||||||
|
<TabControl ItemsSource="{Binding EmojiGroups, ElementName=StackPanel_INTERNAL}" Padding="0">
|
||||||
|
<TabControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid>
|
||||||
|
<controls:EmojiControl Height="24" Emoji="{Binding Icon}">
|
||||||
|
<Image.ToolTip>
|
||||||
|
<TextBlock Text="{Binding Name}"/>
|
||||||
|
</Image.ToolTip>
|
||||||
|
</controls:EmojiControl>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</TabControl.ItemTemplate>
|
||||||
|
<TabControl.ContentTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<ListView Name="EmojiListView" ItemsSource="{Binding EmojiChunkList}"
|
||||||
|
BorderThickness="0"
|
||||||
|
MaxHeight="320" Height="Auto" Padding="0" Margin="1"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<ListView.ItemContainerStyle>
|
||||||
|
<Style TargetType="{x:Type ListViewItem}">
|
||||||
|
<!-- Get rid of the ugly padding and margin in default ListViewItem -->
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="Margin" Value="0"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<!-- Remove the mouse over highlight -->
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="{x:Type ListViewItem}">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ListView.ItemContainerStyle>
|
||||||
|
<!-- Now for our actual content -->
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<ItemsControl ItemsSource="{Binding}"
|
||||||
|
ItemTemplate="{StaticResource ResourceKey=CellTemplate}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
</ItemsControl>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</TabControl.ContentTemplate>
|
||||||
|
</TabControl>
|
||||||
|
</Popup>
|
||||||
|
</StackPanel>
|
123
src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs
Normal file
123
src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
//
|
||||||
|
// Emoji.Wpf — Emoji support for WPF
|
||||||
|
//
|
||||||
|
// Copyright © 2017—2021 Sam Hocevar <sam@hocevar.net>
|
||||||
|
//
|
||||||
|
// This program is free software. It comes without any warranty, to
|
||||||
|
// the extent permitted by applicable law. You can redistribute it
|
||||||
|
// and/or modify it under the terms of the Do What the Fuck You Want
|
||||||
|
// to Public License, Version 2, as published by the WTFPL Task Force.
|
||||||
|
// See http://www.wtfpl.net/ for more details.
|
||||||
|
//
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace Greenshot.Editor.Controls
|
||||||
|
{
|
||||||
|
public class EmojiPickedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public EmojiPickedEventArgs() { }
|
||||||
|
public EmojiPickedEventArgs(string emoji) => Emoji = emoji;
|
||||||
|
|
||||||
|
public string Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void EmojiPickedEventHandler(object sender, EmojiPickedEventArgs e);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for Picker.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class EmojiPicker : StackPanel
|
||||||
|
{
|
||||||
|
public EmojiPicker()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<EmojiData.Group> EmojiGroups => EmojiData.AllGroups;
|
||||||
|
|
||||||
|
// Backwards compatibility for when the backend was a TextBlock.
|
||||||
|
public double FontSize
|
||||||
|
{
|
||||||
|
get => Image.Height * 0.75;
|
||||||
|
set => Image.Height = value / 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler SelectionChanged;
|
||||||
|
|
||||||
|
public event EmojiPickedEventHandler Picked;
|
||||||
|
|
||||||
|
private static void OnSelectionPropertyChanged(DependencyObject source,
|
||||||
|
DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
(source as EmojiPicker)?.OnSelectionChanged(e.NewValue as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Selection
|
||||||
|
{
|
||||||
|
get => (string)GetValue(SelectionProperty);
|
||||||
|
set => SetValue(SelectionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSelectionChanged(string s)
|
||||||
|
{
|
||||||
|
var is_disabled = string.IsNullOrEmpty(s);
|
||||||
|
Image.Emoji = is_disabled ? "???" : s;
|
||||||
|
Image.Opacity = is_disabled ? 0.3 : 1.0;
|
||||||
|
SelectionChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Selection)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEmojiPicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Control control && control.DataContext is EmojiData.Emoji emoji)
|
||||||
|
{
|
||||||
|
if (emoji.VariationList.Count == 0 || sender is Button)
|
||||||
|
{
|
||||||
|
Selection = emoji.Text;
|
||||||
|
Button_INTERNAL.IsChecked = false;
|
||||||
|
e.Handled = true;
|
||||||
|
Picked?.Invoke(this, new EmojiPickedEventArgs(Selection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty SelectionProperty = DependencyProperty.Register(
|
||||||
|
nameof(Selection), typeof(string), typeof(EmojiPicker),
|
||||||
|
new FrameworkPropertyMetadata("☺", OnSelectionPropertyChanged));
|
||||||
|
|
||||||
|
private void OnPopupKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Escape && sender is Popup popup)
|
||||||
|
{
|
||||||
|
popup.IsOpen = false;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPopupLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!(sender is Popup popup))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var child = popup.Child;
|
||||||
|
IInputElement old_focus = null;
|
||||||
|
child.Focusable = true;
|
||||||
|
child.IsVisibleChanged += (o, ea) =>
|
||||||
|
{
|
||||||
|
if (child.IsVisible)
|
||||||
|
{
|
||||||
|
old_focus = Keyboard.FocusedElement;
|
||||||
|
Keyboard.Focus(child);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
popup.Closed += (o, ea) => Keyboard.Focus(old_focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,9 +30,9 @@ using System.Windows.Forms;
|
||||||
using System.Windows.Forms.Integration;
|
using System.Windows.Forms.Integration;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using Emoji.Wpf;
|
|
||||||
using Greenshot.Base.Core;
|
using Greenshot.Base.Core;
|
||||||
using Greenshot.Base.Interfaces.Drawing;
|
using Greenshot.Base.Interfaces.Drawing;
|
||||||
|
using Greenshot.Editor.Controls;
|
||||||
using Greenshot.Editor.Helpers;
|
using Greenshot.Editor.Helpers;
|
||||||
using Image = System.Drawing.Image;
|
using Image = System.Drawing.Image;
|
||||||
using Matrix = System.Drawing.Drawing2D.Matrix;
|
using Matrix = System.Drawing.Drawing2D.Matrix;
|
||||||
|
@ -48,7 +48,7 @@ namespace Greenshot.Editor.Drawing
|
||||||
{
|
{
|
||||||
[NonSerialized] private static EmojiContainer _currentContainer;
|
[NonSerialized] private static EmojiContainer _currentContainer;
|
||||||
[NonSerialized] private static ElementHost _emojiPickerHost;
|
[NonSerialized] private static ElementHost _emojiPickerHost;
|
||||||
[NonSerialized] private static Picker _emojiPicker;
|
[NonSerialized] private static EmojiPicker _emojiPicker;
|
||||||
|
|
||||||
[NonSerialized] private bool _justCreated = true;
|
[NonSerialized] private bool _justCreated = true;
|
||||||
[NonSerialized] private Image _cachedImage = null;
|
[NonSerialized] private Image _cachedImage = null;
|
||||||
|
@ -106,7 +106,7 @@ namespace Greenshot.Editor.Drawing
|
||||||
_emojiPickerHost = _parent.Controls.Find("EmojiPickerHost", false).OfType<ElementHost>().FirstOrDefault();
|
_emojiPickerHost = _parent.Controls.Find("EmojiPickerHost", false).OfType<ElementHost>().FirstOrDefault();
|
||||||
if (_emojiPickerHost == null)
|
if (_emojiPickerHost == null)
|
||||||
{
|
{
|
||||||
_emojiPicker = new Picker();
|
_emojiPicker = new EmojiPicker();
|
||||||
_emojiPicker.Picked += (_, args) =>
|
_emojiPicker.Picked += (_, args) =>
|
||||||
{
|
{
|
||||||
_currentContainer.Emoji = args.Emoji;
|
_currentContainer.Emoji = args.Emoji;
|
||||||
|
@ -207,19 +207,7 @@ namespace Greenshot.Editor.Drawing
|
||||||
|
|
||||||
private Image ComputeBitmap(int iconSize)
|
private Image ComputeBitmap(int iconSize)
|
||||||
{
|
{
|
||||||
// Create WPF control that will be used to render the emoji
|
return EmojiRenderer.GetBitmap(Emoji, iconSize);
|
||||||
var image = new System.Windows.Controls.Image();
|
|
||||||
global::Emoji.Wpf.Image.SetSource(image, Emoji);
|
|
||||||
|
|
||||||
image.RenderTransformOrigin = new System.Windows.Point(0.5, 0.5);
|
|
||||||
image.RenderTransform = new RotateTransform(_rotationAngle);
|
|
||||||
image.Measure(new Size(iconSize, iconSize));
|
|
||||||
image.Arrange(new Rect(0, 0, iconSize, iconSize));
|
|
||||||
|
|
||||||
var renderTargetBitmap = new RenderTargetBitmap(iconSize, iconSize, 96, 96, PixelFormats.Pbgra32);
|
|
||||||
renderTargetBitmap.Render(image);
|
|
||||||
|
|
||||||
return renderTargetBitmap.ToBitmap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetCachedBitmap()
|
private void ResetCachedBitmap()
|
||||||
|
@ -231,9 +219,9 @@ namespace Greenshot.Editor.Drawing
|
||||||
|
|
||||||
internal static class PickerExtensions
|
internal static class PickerExtensions
|
||||||
{
|
{
|
||||||
public static void ShowPopup(this Picker picker, bool show)
|
public static void ShowPopup(this EmojiPicker emojiPicker, bool show)
|
||||||
{
|
{
|
||||||
foreach (var child in picker.Children)
|
foreach (var child in emojiPicker.Children)
|
||||||
{
|
{
|
||||||
if (child is ToggleButton button)
|
if (child is ToggleButton button)
|
||||||
{
|
{
|
||||||
|
|
86
src/Greenshot.Editor/Drawing/EmojiRenderer.cs
Normal file
86
src/Greenshot.Editor/Drawing/EmojiRenderer.cs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using SixLabors.Fonts;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Advanced;
|
||||||
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
|
using SixLabors.ImageSharp.Formats.Png;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using FontStyle = SixLabors.Fonts.FontStyle;
|
||||||
|
using Image = System.Drawing.Image;
|
||||||
|
|
||||||
|
namespace Greenshot.Editor.Drawing
|
||||||
|
{
|
||||||
|
internal static class EmojiRenderer
|
||||||
|
{
|
||||||
|
private static SixLabors.Fonts.FontFamily? _fontFamily;
|
||||||
|
|
||||||
|
public static Image<Rgba32> GetImage(string emoji, int iconSize)
|
||||||
|
{
|
||||||
|
if (_fontFamily == null)
|
||||||
|
{
|
||||||
|
using var stream = Assembly.GetCallingAssembly().GetManifestResourceStream("Greenshot.Editor.Resources.TwemojiMozilla.ttf");
|
||||||
|
var fontCollection = new FontCollection();
|
||||||
|
fontCollection.Add(stream);
|
||||||
|
if (fontCollection.TryGet("Twemoji Mozilla", out var fontFamily))
|
||||||
|
{
|
||||||
|
_fontFamily = fontFamily;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var font = _fontFamily.Value.CreateFont(iconSize, FontStyle.Regular);
|
||||||
|
var image = new Image<Rgba32>(iconSize, iconSize);
|
||||||
|
|
||||||
|
var textOptions = new TextOptions(font) { Origin = new SixLabors.ImageSharp.PointF(0, iconSize / 2.0f), HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center };
|
||||||
|
|
||||||
|
image.Mutate(x => x.DrawText(textOptions, emoji, SixLabors.ImageSharp.Color.Black));
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Image GetBitmap(string emoji, int iconSize)
|
||||||
|
{
|
||||||
|
using var image = GetImage(emoji, iconSize);
|
||||||
|
return image.ToBitmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BitmapSource GetBitmapSource(string emoji, int iconSize)
|
||||||
|
{
|
||||||
|
using var image = GetImage(emoji, iconSize);
|
||||||
|
return image.ToBitmapSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bitmap ToBitmap(this Image<Rgba32> image)
|
||||||
|
{
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
var imageEncoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance);
|
||||||
|
image.Save(memoryStream, imageEncoder);
|
||||||
|
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
return new Bitmap(memoryStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BitmapSource ToBitmapSource(this Image<Rgba32> image)
|
||||||
|
{
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
var imageEncoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance);
|
||||||
|
image.Save(memoryStream, imageEncoder);
|
||||||
|
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var bitmap = new BitmapImage();
|
||||||
|
bitmap.BeginInit();
|
||||||
|
bitmap.StreamSource = memoryStream;
|
||||||
|
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
|
bitmap.EndInit();
|
||||||
|
bitmap.Freeze();
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Emoji.Wpf" Version="0.3.3" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.0.0-alpha.0.156" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -83,9 +83,16 @@
|
||||||
<EmbeddedResource Update="Forms\ImageEditorForm.resx">
|
<EmbeddedResource Update="Forms\ImageEditorForm.resx">
|
||||||
<DependentUpon>ImageEditorForm.cs</DependentUpon>
|
<DependentUpon>ImageEditorForm.cs</DependentUpon>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Resources\emoji-test.txt" />
|
||||||
|
<EmbeddedResource Include="Resources\TwemojiMozilla.ttf" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Resources\" />
|
<Reference Include="SixLabors.Fonts">
|
||||||
|
<HintPath>..\SixLabors.Fonts.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="SixLabors.ImageSharp.Drawing">
|
||||||
|
<HintPath>..\SixLabors.ImageSharp.Drawing.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
BIN
src/Greenshot.Editor/Resources/TwemojiMozilla.ttf
Normal file
BIN
src/Greenshot.Editor/Resources/TwemojiMozilla.ttf
Normal file
Binary file not shown.
4879
src/Greenshot.Editor/Resources/emoji-test.txt
Normal file
4879
src/Greenshot.Editor/Resources/emoji-test.txt
Normal file
File diff suppressed because it is too large
Load diff
BIN
src/SixLabors.Fonts.dll
Normal file
BIN
src/SixLabors.Fonts.dll
Normal file
Binary file not shown.
BIN
src/SixLabors.ImageSharp.Drawing.dll
Normal file
BIN
src/SixLabors.ImageSharp.Drawing.dll
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue