summaryrefslogtreecommitdiff
path: root/Xamarin.Forms.Platform.WP8/FormsPhoneTextBox.cs
diff options
context:
space:
mode:
authorJason Smith <jason.smith@xamarin.com>2016-03-22 20:02:25 (GMT)
committerJason Smith <jason.smith@xamarin.com>2016-03-22 23:13:41 (GMT)
commit17fdde66d94155fc62a034fa6658995bef6fd6e5 (patch)
treeb5e5073a2a7b15cdbe826faa5c763e270a505729 /Xamarin.Forms.Platform.WP8/FormsPhoneTextBox.cs
downloadxamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.zip
xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.tar.gz
xamarin-forms-17fdde66d94155fc62a034fa6658995bef6fd6e5.tar.bz2
Initial import
Diffstat (limited to 'Xamarin.Forms.Platform.WP8/FormsPhoneTextBox.cs')
-rw-r--r--Xamarin.Forms.Platform.WP8/FormsPhoneTextBox.cs236
1 files changed, 236 insertions, 0 deletions
diff --git a/Xamarin.Forms.Platform.WP8/FormsPhoneTextBox.cs b/Xamarin.Forms.Platform.WP8/FormsPhoneTextBox.cs
new file mode 100644
index 0000000..342646c
--- /dev/null
+++ b/Xamarin.Forms.Platform.WP8/FormsPhoneTextBox.cs
@@ -0,0 +1,236 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+using Microsoft.Phone.Controls;
+
+namespace Xamarin.Forms.Platform.WinPhone
+{
+ /// <summary>
+ /// An intermediate class for injecting bindings for things the default
+ /// textbox doesn't allow us to bind/modify
+ /// </summary>
+ public class FormsPhoneTextBox : PhoneTextBox
+ {
+ const char ObfuscationCharacter = '●';
+
+ public static readonly DependencyProperty PlaceholderForegroundBrushProperty = DependencyProperty.Register("PlaceholderForegroundBrush", typeof(Brush), typeof(FormsPhoneTextBox),
+ new PropertyMetadata(default(Brush)));
+
+ public static readonly DependencyProperty IsPasswordProperty = DependencyProperty.Register("IsPassword", typeof(bool), typeof(FormsPhoneTextBox),
+ new PropertyMetadata(default(bool), OnIsPasswordChanged));
+
+ public new static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(FormsPhoneTextBox), new PropertyMetadata("", TextPropertyChanged));
+
+ protected internal static readonly DependencyProperty DisabledTextProperty = DependencyProperty.Register("DisabledText", typeof(string), typeof(FormsPhoneTextBox), new PropertyMetadata(""));
+
+ static InputScope s_passwordInputScope;
+ InputScope _cachedInputScope;
+ CancellationTokenSource _cts;
+ bool _internalChangeFlag;
+
+ public FormsPhoneTextBox()
+ {
+ TextChanged += OnTextChanged;
+ SelectionChanged += OnSelectionChanged;
+ }
+
+ public bool IsPassword
+ {
+ get { return (bool)GetValue(IsPasswordProperty); }
+ set { SetValue(IsPasswordProperty, value); }
+ }
+
+ public Brush PlaceholderForegroundBrush
+ {
+ get { return (Brush)GetValue(PlaceholderForegroundBrushProperty); }
+ set { SetValue(PlaceholderForegroundBrushProperty, value); }
+ }
+
+ public new string Text
+ {
+ get { return (string)GetValue(TextProperty); }
+ set { SetValue(TextProperty, value); }
+ }
+
+ protected internal string DisabledText
+ {
+ get { return (string)GetValue(DisabledTextProperty); }
+ set { SetValue(DisabledTextProperty, value); }
+ }
+
+ static InputScope PasswordInputScope
+ {
+ get
+ {
+ if (s_passwordInputScope != null)
+ return s_passwordInputScope;
+
+ s_passwordInputScope = new InputScope();
+ var name = new InputScopeName { NameValue = InputScopeNameValue.Default };
+ s_passwordInputScope.Names.Add(name);
+
+ return s_passwordInputScope;
+ }
+ }
+
+ void DelayObfuscation()
+ {
+ int lengthDifference = base.Text.Length - Text.Length;
+
+ string updatedRealText = DetermineTextFromPassword(Text, base.Text);
+
+ if (Text == updatedRealText)
+ {
+ // Nothing to do
+ return;
+ }
+
+ Text = updatedRealText;
+
+ // Cancel any pending delayed obfuscation
+ _cts?.Cancel();
+ _cts = null;
+
+ string newText;
+
+ if (lengthDifference != 1)
+ {
+ // Either More than one character got added in this text change (e.g., a paste operation)
+ // Or characters were removed. Either way, we don't need to do the delayed obfuscation dance
+ newText = Obfuscate();
+ }
+ else
+ {
+ // Only one character was added; we need to leave it visible for a brief time period
+ // Obfuscate all but the last character for now
+ newText = Obfuscate(true);
+
+ // Leave the last character visible until a new character is added
+ // or sufficient time has passed
+ if (_cts == null)
+ _cts = new CancellationTokenSource();
+
+ Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromSeconds(0.5), _cts.Token);
+ _cts.Token.ThrowIfCancellationRequested();
+ Dispatcher.BeginInvoke(() =>
+ {
+ base.Text = Obfuscate();
+ SelectionStart = base.Text.Length;
+ });
+ }, _cts.Token);
+ }
+
+ if (base.Text == newText)
+ return;
+
+ base.Text = newText;
+ SelectionStart = base.Text.Length;
+ }
+
+ static string DetermineTextFromPassword(string realText, string passwordText)
+ {
+ int firstObfuscationChar = passwordText.IndexOf(ObfuscationCharacter);
+
+ if (firstObfuscationChar > 0)
+ {
+ // The user is typing faster than we can process, and the text is coming in at the beginning
+ // of the textbox instead of the end
+ passwordText = passwordText.Substring(firstObfuscationChar, passwordText.Length - firstObfuscationChar) + passwordText.Substring(0, firstObfuscationChar);
+ }
+
+ if (realText.Length == passwordText.Length)
+ return realText;
+
+ if (passwordText.Length == 0)
+ return "";
+
+ if (passwordText.Length < realText.Length)
+ return realText.Substring(0, passwordText.Length);
+
+ int lengthDifference = passwordText.Length - realText.Length;
+
+ return realText + passwordText.Substring(passwordText.Length - lengthDifference, lengthDifference);
+ }
+
+ string Obfuscate(bool leaveLastVisible = false)
+ {
+ if (leaveLastVisible && Text.Length == 1)
+ return Text;
+
+ if (leaveLastVisible && Text.Length > 1)
+ return new string(ObfuscationCharacter, Text.Length - 1) + Text.Substring(Text.Length - 1, 1);
+
+ return new string(ObfuscationCharacter, Text.Length);
+ }
+
+ static void OnIsPasswordChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
+ {
+ var textBox = (FormsPhoneTextBox)dependencyObject;
+ textBox.UpdateInputScope();
+ textBox.SyncBaseText();
+ }
+
+ void OnSelectionChanged(object sender, RoutedEventArgs routedEventArgs)
+ {
+ if (!IsPassword)
+ return;
+
+ // Prevent the user from selecting any text in the password box by forcing all selection
+ // to zero-length at the end of the text
+ // This simulates the "do not allow clipboard copy" behavior the PasswordBox control has
+ if (SelectionLength > 0 || SelectionStart < Text.Length)
+ {
+ SelectionLength = 0;
+ SelectionStart = Text.Length;
+ }
+ }
+
+ void OnTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs textChangedEventArgs)
+ {
+ if (IsPassword)
+ DelayObfuscation();
+ else if (base.Text != Text)
+ {
+ // Not in password mode, so we just need to make the "real" Text match
+ // what's in the textbox; the internalChange flag keeps the TextProperty
+ // synchronization from happening
+ _internalChangeFlag = true;
+ Text = base.Text;
+ _internalChangeFlag = false;
+ }
+ }
+
+ void SyncBaseText()
+ {
+ if (_internalChangeFlag)
+ return;
+
+ base.Text = IsPassword ? Obfuscate() : Text;
+ DisabledText = base.Text;
+
+ SelectionStart = base.Text.Length;
+ }
+
+ static void TextPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
+ {
+ var textBox = (FormsPhoneTextBox)dependencyObject;
+ textBox.SyncBaseText();
+ }
+
+ void UpdateInputScope()
+ {
+ if (IsPassword)
+ {
+ _cachedInputScope = InputScope;
+ InputScope = PasswordInputScope; // We don't want suggestions turned on if we're in password mode
+ }
+ else
+ InputScope = _cachedInputScope;
+ }
+ }
+} \ No newline at end of file