mirror of
https://github.com/awatertrevi/xamarin-neo4j.git
synced 2026-09-22 09:05:29 +00:00
Feature: improved symbols bar on Android, auto-capitalize keywords on both platforms
Android symbols bar:
- Rewrite using GlobalLayout + GetWindowVisibleDisplayFrame for reliable keyboard detection
- Toolbar attached once to content view, toggled via visibility (fixes crash)
- Pill-shaped container with themed colors, scrollable symbol strip
- Blue circle play icon (centered TextView), disabled when query empty
- Add symbols: { } \" ' . = * $
iOS symbols bar:
- Add same extra symbols to match Android
- Hide accessory when keyboard dismissed via external gesture
Auto-capitalize (both platforms):
- Cypher keywords auto-uppercased after cursor moves past them
- Respects auto_capitalize preference from Settings
- Skips capitalization while cursor is at end of word (no fighting while typing)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,12 +8,15 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
|
using Android.Content.Res;
|
||||||
using Android.Graphics;
|
using Android.Graphics;
|
||||||
|
using Android.Graphics.Drawables;
|
||||||
using Android.Text;
|
using Android.Text;
|
||||||
using Android.Text.Style;
|
using Android.Text.Style;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Android.Views.InputMethods;
|
using Android.Views.InputMethods;
|
||||||
using Android.Widget;
|
using Android.Widget;
|
||||||
|
using AndroidX.Core.View;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
using Microsoft.Maui.Platform;
|
using Microsoft.Maui.Platform;
|
||||||
using Xamarin.Neo4j.Controls;
|
using Xamarin.Neo4j.Controls;
|
||||||
@@ -49,6 +52,9 @@ namespace Xamarin.Neo4j.Android.CustomRenderers
|
|||||||
"FALSE", "NULL", "TRUE"
|
"FALSE", "NULL", "TRUE"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private LinearLayout _toolbar;
|
||||||
|
private bool _toolbarAttached;
|
||||||
|
|
||||||
protected override void ConnectHandler(MauiAppCompatEditText platformView)
|
protected override void ConnectHandler(MauiAppCompatEditText platformView)
|
||||||
{
|
{
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
@@ -58,43 +64,133 @@ namespace Xamarin.Neo4j.Android.CustomRenderers
|
|||||||
| InputTypes.TextFlagMultiLine
|
| InputTypes.TextFlagMultiLine
|
||||||
| InputTypes.TextFlagNoSuggestions;
|
| InputTypes.TextFlagNoSuggestions;
|
||||||
|
|
||||||
// Keyboard toolbar with symbol keys and Execute button
|
// Build the keyboard toolbar
|
||||||
var toolbar = BuildToolbar(platformView);
|
_toolbar = BuildToolbar(platformView);
|
||||||
|
|
||||||
|
// Attach toolbar once to the content view (stays in hierarchy, just hidden)
|
||||||
|
AttachToolbarOnce(platformView);
|
||||||
|
|
||||||
|
// Detect keyboard via visible frame height difference.
|
||||||
|
// Edge-to-edge and varying MAUI soft-input modes make WindowInsets unreliable,
|
||||||
|
// so we compare the root view height to the visible display frame.
|
||||||
|
platformView.ViewTreeObserver.GlobalLayout += (s, e) =>
|
||||||
|
{
|
||||||
|
var rootView = platformView.RootView;
|
||||||
|
if (rootView == null) return;
|
||||||
|
|
||||||
|
var rect = new global::Android.Graphics.Rect();
|
||||||
|
rootView.GetWindowVisibleDisplayFrame(rect);
|
||||||
|
var screenHeight = rootView.Height;
|
||||||
|
var keyboardHeight = screenHeight - rect.Bottom;
|
||||||
|
|
||||||
|
if (keyboardHeight > screenHeight * 0.15 && platformView.HasFocus)
|
||||||
|
{
|
||||||
|
if (_toolbar.LayoutParameters is FrameLayout.LayoutParams lp)
|
||||||
|
{
|
||||||
|
lp.BottomMargin = keyboardHeight;
|
||||||
|
_toolbar.LayoutParameters = lp;
|
||||||
|
}
|
||||||
|
_toolbar.Visibility = ViewStates.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_toolbar.Visibility = ViewStates.Gone;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also hide on focus loss
|
||||||
platformView.FocusChange += (s, e) =>
|
platformView.FocusChange += (s, e) =>
|
||||||
toolbar.Visibility = e.HasFocus ? ViewStates.Visible : ViewStates.Gone;
|
{
|
||||||
|
if (!e.HasFocus)
|
||||||
|
_toolbar.Visibility = ViewStates.Gone;
|
||||||
|
};
|
||||||
|
|
||||||
// Syntax highlighting
|
// Syntax highlighting
|
||||||
platformView.AddTextChangedListener(new CypherTextWatcher(platformView, _keyWords));
|
platformView.AddTextChangedListener(new CypherTextWatcher(platformView, _keyWords));
|
||||||
HighlightSyntax(platformView, _keyWords);
|
HighlightSyntax(platformView, _keyWords);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AttachToolbarOnce(MauiAppCompatEditText platformView)
|
||||||
|
{
|
||||||
|
if (_toolbarAttached) return;
|
||||||
|
|
||||||
|
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||||
|
var decorView = activity?.Window?.DecorView as ViewGroup;
|
||||||
|
var contentView = decorView?.FindViewById<FrameLayout>(global::Android.Resource.Id.Content);
|
||||||
|
if (contentView == null) return;
|
||||||
|
|
||||||
|
var layoutParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MatchParent,
|
||||||
|
ViewGroup.LayoutParams.WrapContent,
|
||||||
|
GravityFlags.Bottom);
|
||||||
|
contentView.AddView(_toolbar, layoutParams);
|
||||||
|
_toolbar.Visibility = ViewStates.Gone;
|
||||||
|
_toolbarAttached = true;
|
||||||
|
}
|
||||||
|
|
||||||
private LinearLayout BuildToolbar(MauiAppCompatEditText platformView)
|
private LinearLayout BuildToolbar(MauiAppCompatEditText platformView)
|
||||||
{
|
{
|
||||||
var context = platformView.Context;
|
var context = platformView.Context;
|
||||||
|
var density = context.Resources.DisplayMetrics.Density;
|
||||||
|
|
||||||
var toolbar = new LinearLayout(context)
|
var toolbar = new LinearLayout(context)
|
||||||
{
|
{
|
||||||
Orientation = Orientation.Horizontal,
|
Orientation = global::Android.Widget.Orientation.Horizontal,
|
||||||
LayoutParameters = new LinearLayout.LayoutParams(
|
LayoutParameters = new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MatchParent,
|
ViewGroup.LayoutParams.MatchParent,
|
||||||
ViewGroup.LayoutParams.WrapContent)
|
(int)(48 * density))
|
||||||
};
|
};
|
||||||
toolbar.SetBackgroundColor(Color.ParseColor("#F2F2F7"));
|
|
||||||
toolbar.SetPadding(8, 8, 8, 8);
|
|
||||||
toolbar.Visibility = ViewStates.Gone;
|
|
||||||
|
|
||||||
// Scrollable symbol key strip
|
// Theme-aware background
|
||||||
|
var isDark = (context.Resources.Configuration.UiMode & UiMode.NightMask) == UiMode.NightYes;
|
||||||
|
toolbar.SetBackgroundColor(Color.ParseColor(isDark ? "#141414" : "#F2F2F7"));
|
||||||
|
toolbar.SetPadding((int)(6 * density), (int)(6 * density), (int)(6 * density), (int)(6 * density));
|
||||||
|
toolbar.SetGravity(GravityFlags.CenterVertical);
|
||||||
|
|
||||||
|
// Pill container for symbol keys
|
||||||
|
var pillContainer = new LinearLayout(context)
|
||||||
|
{
|
||||||
|
Orientation = global::Android.Widget.Orientation.Horizontal,
|
||||||
|
LayoutParameters = new LinearLayout.LayoutParams(
|
||||||
|
0, (int)(36 * density), 1f)
|
||||||
|
};
|
||||||
|
var pillBg = new GradientDrawable();
|
||||||
|
pillBg.SetCornerRadius(18 * density);
|
||||||
|
pillBg.SetColor(Color.ParseColor(isDark ? "#2a2a2a" : "#E8E8ED").ToArgb());
|
||||||
|
pillContainer.Background = pillBg;
|
||||||
|
pillContainer.SetGravity(GravityFlags.CenterVertical);
|
||||||
|
|
||||||
|
// Scrollable symbol key strip inside pill
|
||||||
var scrollView = new HorizontalScrollView(context)
|
var scrollView = new HorizontalScrollView(context)
|
||||||
{
|
{
|
||||||
LayoutParameters = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WrapContent, 1f)
|
LayoutParameters = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)
|
||||||
};
|
};
|
||||||
scrollView.HorizontalScrollBarEnabled = false;
|
scrollView.HorizontalScrollBarEnabled = false;
|
||||||
|
|
||||||
var keyRow = new LinearLayout(context) { Orientation = Orientation.Horizontal };
|
var keyRow = new LinearLayout(context)
|
||||||
foreach (var key in new[] { "(", ")", "[", "]", ":", "-", "->", "<-" })
|
{
|
||||||
|
Orientation = global::Android.Widget.Orientation.Horizontal,
|
||||||
|
LayoutParameters = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.MatchParent)
|
||||||
|
};
|
||||||
|
keyRow.SetGravity(GravityFlags.CenterVertical);
|
||||||
|
|
||||||
|
var symbolTextColor = Color.ParseColor(isDark ? "#e2e2e2" : "#0c0c0c");
|
||||||
|
foreach (var key in new[] { "(", ")", "[", "]", "{", "}", ":", "-", "->", "<-", "\"", "'", ".", "=", "*", "$" })
|
||||||
{
|
{
|
||||||
var captured = key;
|
var captured = key;
|
||||||
var btn = CreatePillButton(context, captured);
|
var btn = new Button(context)
|
||||||
|
{
|
||||||
|
Text = captured,
|
||||||
|
LayoutParameters = new LinearLayout.LayoutParams(
|
||||||
|
(int)(40 * density), ViewGroup.LayoutParams.MatchParent)
|
||||||
|
};
|
||||||
|
btn.SetTextColor(symbolTextColor);
|
||||||
|
btn.SetBackgroundColor(Color.Transparent);
|
||||||
|
btn.SetTextSize(global::Android.Util.ComplexUnitType.Sp, 15);
|
||||||
|
btn.SetAllCaps(false);
|
||||||
|
btn.SetPadding(0, 0, 0, 0);
|
||||||
btn.Click += (s, e) =>
|
btn.Click += (s, e) =>
|
||||||
{
|
{
|
||||||
var start = Math.Max(platformView.SelectionStart, 0);
|
var start = Math.Max(platformView.SelectionStart, 0);
|
||||||
@@ -105,12 +201,34 @@ namespace Xamarin.Neo4j.Android.CustomRenderers
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollView.AddView(keyRow);
|
scrollView.AddView(keyRow);
|
||||||
toolbar.AddView(scrollView);
|
pillContainer.AddView(scrollView);
|
||||||
|
toolbar.AddView(pillContainer);
|
||||||
|
|
||||||
// Execute button
|
// Spacer
|
||||||
var executeBtn = CreatePillButton(context, "Execute");
|
var spacer = new global::Android.Views.View(context)
|
||||||
|
{
|
||||||
|
LayoutParameters = new LinearLayout.LayoutParams((int)(6 * density), 0)
|
||||||
|
};
|
||||||
|
toolbar.AddView(spacer);
|
||||||
|
|
||||||
|
// Execute button — blue circle with play icon (▶)
|
||||||
|
var executeBtn = new TextView(context)
|
||||||
|
{
|
||||||
|
Text = "\u25B6",
|
||||||
|
LayoutParameters = new LinearLayout.LayoutParams(
|
||||||
|
(int)(36 * density), (int)(36 * density)),
|
||||||
|
Gravity = GravityFlags.Center,
|
||||||
|
Clickable = true,
|
||||||
|
Focusable = true
|
||||||
|
};
|
||||||
executeBtn.SetTextColor(Color.White);
|
executeBtn.SetTextColor(Color.White);
|
||||||
executeBtn.SetBackgroundColor(Color.ParseColor("#007AFF"));
|
executeBtn.SetTextSize(global::Android.Util.ComplexUnitType.Sp, 18);
|
||||||
|
var executeBg = new GradientDrawable();
|
||||||
|
executeBg.SetCornerRadius(18 * density);
|
||||||
|
executeBg.SetColor(Color.ParseColor("#007AFF").ToArgb());
|
||||||
|
executeBtn.Background = executeBg;
|
||||||
|
executeBtn.SetPadding(0, 0, 0, 0);
|
||||||
|
executeBtn.SetIncludeFontPadding(false);
|
||||||
executeBtn.Click += (s, e) =>
|
executeBtn.Click += (s, e) =>
|
||||||
{
|
{
|
||||||
if (VirtualView is QueryEditor queryEditor)
|
if (VirtualView is QueryEditor queryEditor)
|
||||||
@@ -120,33 +238,23 @@ namespace Xamarin.Neo4j.Android.CustomRenderers
|
|||||||
imm?.HideSoftInputFromWindow(platformView.WindowToken, 0);
|
imm?.HideSoftInputFromWindow(platformView.WindowToken, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Disable run button when query is empty
|
||||||
|
void updateRunEnabled()
|
||||||
|
{
|
||||||
|
var hasText = !string.IsNullOrWhiteSpace(platformView.Text);
|
||||||
|
executeBtn.Enabled = hasText;
|
||||||
|
executeBtn.Alpha = hasText ? 1f : 0.35f;
|
||||||
|
}
|
||||||
|
updateRunEnabled();
|
||||||
|
platformView.AddTextChangedListener(new SimpleTextWatcher(updateRunEnabled));
|
||||||
|
|
||||||
toolbar.AddView(executeBtn);
|
toolbar.AddView(executeBtn);
|
||||||
|
|
||||||
// Attach toolbar to the parent view hierarchy
|
toolbar.Visibility = ViewStates.Gone;
|
||||||
platformView.ViewTreeObserver.GlobalLayout += (s, e) =>
|
|
||||||
{
|
|
||||||
if (platformView.Parent is ViewGroup parent && toolbar.Parent == null)
|
|
||||||
parent.AddView(toolbar);
|
|
||||||
};
|
|
||||||
|
|
||||||
return toolbar;
|
return toolbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Button CreatePillButton(Context context, string label)
|
|
||||||
{
|
|
||||||
return new Button(context)
|
|
||||||
{
|
|
||||||
Text = label,
|
|
||||||
LayoutParameters = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.WrapContent,
|
|
||||||
ViewGroup.LayoutParams.WrapContent)
|
|
||||||
{
|
|
||||||
MarginStart = 4,
|
|
||||||
MarginEnd = 4
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Syntax highlighting ───────────────────────────────────────────────
|
// ── Syntax highlighting ───────────────────────────────────────────────
|
||||||
|
|
||||||
internal static void HighlightSyntax(MauiAppCompatEditText platformView, IEnumerable<string> keywords)
|
internal static void HighlightSyntax(MauiAppCompatEditText platformView, IEnumerable<string> keywords)
|
||||||
@@ -191,6 +299,15 @@ namespace Xamarin.Neo4j.Android.CustomRenderers
|
|||||||
|
|
||||||
// ── Inner helpers ─────────────────────────────────────────────────────
|
// ── Inner helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private sealed class SimpleTextWatcher : Java.Lang.Object, ITextWatcher
|
||||||
|
{
|
||||||
|
private readonly Action _callback;
|
||||||
|
public SimpleTextWatcher(Action callback) { _callback = callback; }
|
||||||
|
public void BeforeTextChanged(Java.Lang.ICharSequence s, int start, int count, int after) { }
|
||||||
|
public void OnTextChanged(Java.Lang.ICharSequence s, int start, int before, int count) { }
|
||||||
|
public void AfterTextChanged(IEditable s) { _callback(); }
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class CypherTextWatcher : Java.Lang.Object, ITextWatcher
|
private sealed class CypherTextWatcher : Java.Lang.Object, ITextWatcher
|
||||||
{
|
{
|
||||||
private readonly MauiAppCompatEditText _view;
|
private readonly MauiAppCompatEditText _view;
|
||||||
@@ -210,9 +327,50 @@ namespace Xamarin.Neo4j.Android.CustomRenderers
|
|||||||
{
|
{
|
||||||
if (_updating) return;
|
if (_updating) return;
|
||||||
_updating = true;
|
_updating = true;
|
||||||
try { HighlightSyntax(_view, _keywords); }
|
try
|
||||||
|
{
|
||||||
|
AutoCapitalizeKeywords(_view, _keywords);
|
||||||
|
HighlightSyntax(_view, _keywords);
|
||||||
|
}
|
||||||
finally { _updating = false; }
|
finally { _updating = false; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void AutoCapitalizeKeywords(MauiAppCompatEditText platformView, IEnumerable<string> keywords)
|
||||||
|
{
|
||||||
|
if (!Microsoft.Maui.Storage.Preferences.Default.Get("auto_capitalize", true)) return;
|
||||||
|
|
||||||
|
var text = platformView.Text ?? string.Empty;
|
||||||
|
var selStart = platformView.SelectionStart;
|
||||||
|
var selEnd = platformView.SelectionEnd;
|
||||||
|
var changed = false;
|
||||||
|
var chars = text.ToCharArray();
|
||||||
|
|
||||||
|
foreach (var word in keywords)
|
||||||
|
{
|
||||||
|
var regex = new Regex("\\b" + Regex.Escape(word) + "\\b", RegexOptions.IgnoreCase);
|
||||||
|
foreach (Match match in regex.Matches(text))
|
||||||
|
{
|
||||||
|
// Skip if already uppercase
|
||||||
|
var segment = text.Substring(match.Index, match.Length);
|
||||||
|
if (segment == word) continue;
|
||||||
|
|
||||||
|
// Don't capitalize if cursor is right at the end of this word (user still typing)
|
||||||
|
if (selStart == match.Index + match.Length) continue;
|
||||||
|
|
||||||
|
for (var i = 0; i < match.Length; i++)
|
||||||
|
chars[match.Index + i] = word[i];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
var newText = new string(chars);
|
||||||
|
platformView.SetText(newText, TextView.BufferType.Editable);
|
||||||
|
if (selStart >= 0 && selStart <= newText.Length)
|
||||||
|
platformView.SetSelection(Math.Min(selStart, newText.Length), Math.Min(selEnd, newText.Length));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ namespace Xamarin.Neo4j.iOS.CustomRenderers
|
|||||||
var keys = new[]
|
var keys = new[]
|
||||||
{
|
{
|
||||||
("(", "("), (")", ")"), ("[", "["), ("]", "]"),
|
("(", "("), (")", ")"), ("[", "["), ("]", "]"),
|
||||||
(":", ":"), ("-", "-"), ("\u2192", "->"), ("\u2190", "<-")
|
("{", "{"), ("}", "}"), (":", ":"), ("-", "-"),
|
||||||
|
("\u2192", "->"), ("\u2190", "<-"), ("\"", "\""),
|
||||||
|
("'", "'"), (".", "."), ("=", "="), ("*", "*"), ("$", "$")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Outer accessory — clear so system background shows through
|
// Outer accessory — clear so system background shows through
|
||||||
@@ -147,7 +149,21 @@ namespace Xamarin.Neo4j.iOS.CustomRenderers
|
|||||||
|
|
||||||
platformView.InputAccessoryView = accessoryView;
|
platformView.InputAccessoryView = accessoryView;
|
||||||
|
|
||||||
platformView.Changed += (s, e) => HighlightWords(platformView, _keyWords);
|
// Ensure accessory hides when keyboard is dismissed by external gesture/scroll
|
||||||
|
var keyboardHideObserver = UIKeyboard.Notifications.ObserveWillHide((s, args) =>
|
||||||
|
{
|
||||||
|
// The InputAccessoryView is removed with the keyboard automatically.
|
||||||
|
// But if the editor remains first responder after an external dismiss
|
||||||
|
// (e.g. interactive dismiss on scroll), resign to fully hide the bar.
|
||||||
|
if (platformView.IsFirstResponder)
|
||||||
|
platformView.ResignFirstResponder();
|
||||||
|
});
|
||||||
|
|
||||||
|
platformView.Changed += (s, e) =>
|
||||||
|
{
|
||||||
|
AutoCapitalizeKeywords(platformView, _keyWords);
|
||||||
|
HighlightWords(platformView, _keyWords);
|
||||||
|
};
|
||||||
|
|
||||||
HighlightWords(platformView, _keyWords);
|
HighlightWords(platformView, _keyWords);
|
||||||
}
|
}
|
||||||
@@ -192,6 +208,37 @@ namespace Xamarin.Neo4j.iOS.CustomRenderers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AutoCapitalizeKeywords(UITextView platformView, IEnumerable<string> keywords)
|
||||||
|
{
|
||||||
|
if (!Microsoft.Maui.Storage.Preferences.Default.Get("auto_capitalize", true)) return;
|
||||||
|
|
||||||
|
var text = platformView.Text ?? string.Empty;
|
||||||
|
var cursorPos = platformView.SelectedRange;
|
||||||
|
var changed = false;
|
||||||
|
var chars = text.ToCharArray();
|
||||||
|
|
||||||
|
foreach (var word in keywords)
|
||||||
|
{
|
||||||
|
var regex = new Regex("\\b" + Regex.Escape(word) + "\\b", RegexOptions.IgnoreCase);
|
||||||
|
foreach (Match match in regex.Matches(text))
|
||||||
|
{
|
||||||
|
var segment = text.Substring(match.Index, match.Length);
|
||||||
|
if (segment == word) continue;
|
||||||
|
if ((nint)cursorPos.Location == match.Index + match.Length) continue;
|
||||||
|
|
||||||
|
for (var i = 0; i < match.Length; i++)
|
||||||
|
chars[match.Index + i] = word[i];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
platformView.Text = new string(chars);
|
||||||
|
platformView.SelectedRange = cursorPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string PreprocessText(string text)
|
private static string PreprocessText(string text)
|
||||||
{
|
{
|
||||||
text = text.Replace("\u2018", "'");
|
text = text.Replace("\u2018", "'");
|
||||||
|
|||||||
Reference in New Issue
Block a user