diff --git a/src/Notepads/Controls/TextEditor/TextEditorContextFlyout.cs b/src/Notepads/Controls/TextEditor/TextEditorContextFlyout.cs index 2d6a58263..d7796cad7 100644 --- a/src/Notepads/Controls/TextEditor/TextEditorContextFlyout.cs +++ b/src/Notepads/Controls/TextEditor/TextEditorContextFlyout.cs @@ -29,6 +29,13 @@ public sealed class TextEditorContextFlyout : MenuFlyout private MenuFlyoutItem _previewToggle; private MenuFlyoutItem _share; + private MenuFlyoutSubItem _capitalizations; + private MenuFlyoutItem _uppercase; + private MenuFlyoutItem _lowercase; + private MenuFlyoutItem _sentencecase; + private MenuFlyoutItem _togglecase; + private MenuFlyoutItem _titlecase; + private MenuFlyout _proofingFlyout; private readonly MenuFlyoutSeparator _proofingSeparator = new MenuFlyoutSeparator(); @@ -54,6 +61,7 @@ public TextEditorContextFlyout(ITextEditor editor, TextEditorCore editorCore) Items.Add(WebSearch); Items.Add(PreviewToggle); Items.Add(Share); + Items.Add(Capitalizations); Opening += TextEditorContextFlyout_Opening; Closed += TextEditorContextFlyout_Closed; @@ -99,6 +107,10 @@ _textEditorCore.ProofingMenuFlyout is MenuFlyout proofingFlyout && { Share.Visibility = Visibility.Collapsed; } + + _textEditorCore.GetTextSelectionPosition(out var start, out var end); + SentenceCase.IsEnabled = start == end; + TitleCase.IsEnabled = start == end; } private void BuildProofingSubItems(MenuFlyout proofingFlyout) @@ -283,6 +295,125 @@ public MenuFlyoutItem RightToLeftReadingOrder } } + public MenuFlyoutItem UpperCase + { + get + { + if (_uppercase != null) return _uppercase; + + _uppercase = new MenuFlyoutItem + { + Text = _resourceLoader.GetString("TextEditor_ContextFlyout_UpperCaseButtonDisplayText"), + }; + _uppercase.Click += (sender, args) => + { + _textEditorCore.GetTextSelectionPosition(out var startPos, out var endPos); + _textEditorCore.Capitalize(); + _textEditorCore.SetTextSelectionPosition(startPos, endPos); + }; + return _uppercase; + } + } + + public MenuFlyoutItem LowerCase + { + get + { + if (_lowercase != null) return _lowercase; + + _lowercase = new MenuFlyoutItem + { + Text = _resourceLoader.GetString("TextEditor_ContextFlyout_LowerCaseButtonDisplayText"), + }; + _lowercase.Click += (sender, args) => + { + _textEditorCore.GetTextSelectionPosition(out var startPos, out var endPos); + _textEditorCore.Decapitalize(); + _textEditorCore.SetTextSelectionPosition(startPos, endPos); + }; + return _lowercase; + } + } + + public MenuFlyoutItem SentenceCase + { + get + { + if (_sentencecase != null) return _sentencecase; + _sentencecase = new MenuFlyoutItem + { + Text = _resourceLoader.GetString("TextEditor_ContextFlyout_SentenceCaseButtonDisplayText"), + }; + _sentencecase.Click += (sender, args) => + { + _textEditorCore.GetTextSelectionPosition(out var startPos, out var endPos); + _textEditorCore.SentenceCase(); + _textEditorCore.SetTextSelectionPosition(startPos, endPos); + }; + return _sentencecase; + } + } + + public MenuFlyoutItem ToggleCase + { + get + { + if (_togglecase != null) return _togglecase; + _togglecase = new MenuFlyoutItem + { + Text = _resourceLoader.GetString("TextEditor_ContextFlyout_ToggleCaseButtonDisplayText"), + }; + _togglecase.Click += (sender, args) => + { + _textEditorCore.GetTextSelectionPosition(out var startPos, out var endPos); + _textEditorCore.ToggleCase(); + _textEditorCore.SetTextSelectionPosition(startPos, endPos); + }; + return _togglecase; + } + } + + public MenuFlyoutItem TitleCase + { + get + { + if (_titlecase != null) return _titlecase; + _titlecase = new MenuFlyoutItem + { + Text = _resourceLoader.GetString("TextEditor_ContextFlyout_TitleCaseButtonDisplayText"), + }; + _titlecase.Click += (sender, args) => + { + _textEditorCore.GetTextSelectionPosition(out var startPos, out var endPos); + _textEditorCore.TitleCase(); + _textEditorCore.SetTextSelectionPosition(startPos, endPos); + }; + return _titlecase; + } + } + + public MenuFlyoutSubItem Capitalizations + { + get + { + if (_capitalizations != null) return _capitalizations; + + _capitalizations = new MenuFlyoutSubItem + { + Text = _resourceLoader.GetString("TextEditor_ContextFlyout_CapitalizationsButtonDisplayText"), + }; + + _capitalizations.Items.Add(UpperCase); + _capitalizations.Items.Add(LowerCase); + _capitalizations.Items.Add(ToggleCase); + _capitalizations.Items.Add(new MenuFlyoutSeparator()); + _capitalizations.Items.Add(TitleCase); + _capitalizations.Items.Add(SentenceCase); + + return _capitalizations; + } + } + public MenuFlyoutItem WebSearch { get diff --git a/src/Notepads/Controls/TextEditor/TextEditorCore.Capitalizations.cs b/src/Notepads/Controls/TextEditor/TextEditorCore.Capitalizations.cs new file mode 100644 index 000000000..838037545 --- /dev/null +++ b/src/Notepads/Controls/TextEditor/TextEditorCore.Capitalizations.cs @@ -0,0 +1,204 @@ +// --------------------------------------------------------------------------------------------- +// Copyright (c) 2019-2024, Jiaqi (0x7c13) Liu. All rights reserved. +// See LICENSE file in the project root for license information. +// --------------------------------------------------------------------------------------------- + +namespace Notepads.Controls.TextEditor +{ + using Windows.UI.Text; + using System.Text; + + public partial class TextEditorCore + { + public void Capitalize() + { + if (Document.Selection.Length != 0) + { + Document.Selection.ChangeCase(LetterCase.Upper); + } + else + { + Document.GetText(TextGetOptions.None, out var text); + Document.SetText(TextSetOptions.None, text.TrimEnd().ToUpperInvariant()); + } + } + + public void Decapitalize() + { + if (Document.Selection.Length != 0) + { + Document.Selection.ChangeCase(LetterCase.Lower); + } + else + { + Document.GetText(TextGetOptions.None, out var text); + Document.SetText(TextSetOptions.None, text.TrimEnd().ToLowerInvariant()); + } + } + + public void SentenceCase() + { + if (Document.Selection.Length != 0) + { + return; + } + + Document.GetText(TextGetOptions.None, out var text); + + if (string.IsNullOrEmpty(text)) + { + return; + } + + var sb = new StringBuilder(text); + bool capitalizeNext = true; + + for (int i = 0; i < sb.Length; i++) + { + var ch = sb[i]; + + if (capitalizeNext) + { + if (char.IsWhiteSpace(ch) || IsOpeningPunctuation(ch)) + { + continue; + } + + if (char.IsLetter(ch)) + { + sb[i] = char.ToUpperInvariant(ch); + } + capitalizeNext = false; + } + else + { + if (IsSentenceEndingPunctuation(ch)) + { + capitalizeNext = true; + } + } + } + + Document.SetText(TextSetOptions.None, sb.ToString().TrimEnd()); + } + + private static bool IsSentenceEndingPunctuation(char c) + { + return c == '.' || c == '!' || c == '?'; + } + + private static bool IsOpeningPunctuation(char c) + { + switch (c) + { + case '"': + case '\'': + case '(': + case '[': + case '<': + case '{': + case '\u2018': // ‘ + case '\u2019': // ’ + case '\u201C': // “ + case '\u201D': // ” + case '\u00AB': // « + case '\u00BB': // » + return true; + + default: + return false; + } + } + + public void TitleCase() + { + if (Document.Selection.Length != 0) + { + Document.Selection.GetText(TextGetOptions.None, out var selectedText); + var output = ToTitleCase(selectedText); + Document.Selection.SetText(TextSetOptions.None, output); + } + else + { + Document.GetText(TextGetOptions.None, out var text); + var output = ToTitleCase(text); + Document.SetText(TextSetOptions.None, output.TrimEnd()); + } + } + + private string ToTitleCase(string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + var sb = new StringBuilder(input.Length); + bool newWord = true; + + foreach (var ch in input) + { + if (char.IsLetter(ch)) + { + if (newWord) + { + sb.Append(char.ToUpperInvariant(ch)); + } + else + { + sb.Append(char.ToLowerInvariant(ch)); + } + newWord = false; + } + else + { + sb.Append(ch); + newWord = true; + } + } + + return sb.ToString(); + } + + public void ToggleCase() + { + if (Document.Selection.Length != 0) + { + Document.Selection.GetText(TextGetOptions.None, out var selectedText); + var output = ToggleCase(selectedText); + Document.Selection.SetText(TextSetOptions.None, output); + } + else + { + Document.GetText(TextGetOptions.None, out var text); + var output = ToggleCase(text); + Document.SetText(TextSetOptions.None, output.TrimEnd()); + } + } + + private string ToggleCase(string text) + { + if (string.IsNullOrEmpty(text)) + { + return string.Empty; + } + var sb = new StringBuilder(text?.Length ?? 0); + foreach (var ch in text) + { + if (char.IsUpper(ch)) + { + sb.Append(char.ToLowerInvariant(ch)); + } + else if (char.IsLower(ch)) + { + sb.Append(char.ToUpperInvariant(ch)); + } + else + { + sb.Append(ch); + } + } + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Notepads/Notepads.csproj b/src/Notepads/Notepads.csproj index 2bc8adb78..b7a373eea 100644 --- a/src/Notepads/Notepads.csproj +++ b/src/Notepads/Notepads.csproj @@ -170,6 +170,7 @@ PrintPageFormat.xaml + diff --git a/src/Notepads/Strings/en-US/Resources.resw b/src/Notepads/Strings/en-US/Resources.resw index 63f824ddf..32eca5107 100644 --- a/src/Notepads/Strings/en-US/Resources.resw +++ b/src/Notepads/Strings/en-US/Resources.resw @@ -1,17 +1,17 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Cancel AppCloseSaveReminderDialog: CloseButtonText. - + There are unsaved changes. AppCloseSaveReminderDialog: "Content" display text. - + Save All & Exit AppCloseSaveReminderDialog: PrimaryButtonText. - + Discard & Exit AppCloseSaveReminderDialog: SecondaryButtonText. - + Do you want to save the changes? AppCloseSaveReminderDialog: "Title" display text. - + Nothing to share because no text is selected and current document is empty. ContentSharing: Failure message when user trying to share empty content. - + After your changes DiffViewer: Header's text for new text (After changes). - + Before your changes DiffViewer: Header's text for old text (Before changes). - + Notepads does not support file greater than 1MB at this moment. ErrorMessage: NotepadsFileSizeLimit text. - + Sorry, file "{0}" couldn't be opened: {1} FileOpenErrorDialog: "Content" display text, {0} stands for file path, {1} stands for error message. You can change the order but DO NOT REMOVE them from the string. - + Ok FileOpenErrorDialog: PrimaryButtonText. - + File Open Error FileOpenErrorDialog: "Title" display text. - + Sorry, file "{0}" couldn't be saved: {1} FileSaveErrorDialog: "Content" display text, {0} stands for file path, {1} stands for error message. You can change the order but DO NOT REMOVE them from the string. - + Ok FileSaveErrorDialog: PrimaryButtonText. - + File Save Error FileSaveErrorDialog: "Title" display text. - + Close FindAndReplace: "Dismiss" button tool tip display text. - + Find FindAndReplace: Find bar placeholder text. - + Not Found FindAndReplace: Notification message when target not found. - + Replace All (Ctrl+Alt+Enter) FindAndReplace: "Replace All" button tool tip display text. - + Replace FindAndReplace: Replace bar placeholder text. - + Replace (Alt+R) FindAndReplace: "Replace" button tool tip display text. - + Find Next (F3) FindAndReplace: "Find Next" button tool tip display text. - + Search Options FindAndReplace: "SearchOptions" button tool tip display text. - + Match Case FindAndReplace: "Match Case" OptionToggleButton display text. - + Match Whole Word FindAndReplace: "Match Whole Word" OptionToggleButton display text. - + Find... MainMenu: "Find" button display text. - + New MainMenu: "New" button display text. - + Open... MainMenu: "Open" button display text. - + Print... MainMenu: "Print" button display text. - + Replace... MainMenu: "Replace" button display text. - + Save MainMenu: "Save" button display text. - + Save All MainMenu: "Save All" button display text. - + Save As... MainMenu: "Save As" button display text. - + Settings MainMenu: "Settings" button display text. - + Cancel RevertAllChangesConfirmationDialog: CloseButtonText. - + All changes including text, line ending and encoding made to "{0}" will be reverted! RevertAllChangesConfirmationDialog: "Content" display text, {0} stands for file name. You can change the order but DO NOT REMOVE it from the string. - + Yes RevertAllChangesConfirmationDialog: PrimaryButtonText. - + Are you sure to revert all changes? RevertAllChangesConfirmationDialog: "Title" display text. - + Cancel SetCloseSaveReminderDialog: CloseButtonText. - + Save file "{0}"? SetCloseSaveReminderDialog: "Content" display text. {0} stands for file name/path. You can change the order but DO NOT REMOVE it from the string. - + Save SetCloseSaveReminderDialog: PrimaryButtonText. - + Don't Save SetCloseSaveReminderDialog: SecondaryButtonText. - + Save your changes? SetCloseSaveReminderDialog: "Title" display text. - + Close Tab: ContextFlyout "Close" button display text. - + Close Others Tab: ContextFlyout "Close Others" button display text. - + Close to the Right Tab: ContextFlyout "Close to the Right" button display text. - + Close Saved Tab: ContextFlyout "Close Saved" button display text. - + Copy Full Path Tab: ContextFlyout "Copy Full Path" button display text. - + Open Containing Folder Tab: ContextFlyout "Open Containing Folder" button display text. - + Copy TextEditor: ContextFlyout "Copy" button display text. - + Cut TextEditor: ContextFlyout "Cut" button display text. - + Paste TextEditor: ContextFlyout "Paste" button display text. - + Toggle Preview TextEditor: ContextFlyout "Toggle Preview" button display text. - + Redo TextEditor: ContextFlyout "Redo" button display text. - + Select All TextEditor: ContextFlyout "Select All" button display text. - + Share TextEditor: ContextFlyout "Share" button display text. - + Share Selected TextEditor: ContextFlyout "Share Selected" button display text. - + Undo TextEditor: ContextFlyout "Undo" button display text. - + Word Wrap TextEditor: ContextFlyout "Word Wrap" toggle button display text. - + + Capitalizations + TextEditor: ContextFlyout "Capitalizations" menu button display text. + + + UPPER-CASE + TextEditor: ContextFlyout "Uppercase" toggle button display text. + + + lower-case + TextEditor: ContextFlyout "Lowercase" toggle button display text. + + + Sentence case + TextEditor: ContextFlyout "Sentence case" toggle button display text. + + + tOGGLE cASE + TextEditor: ContextFlyout "Toggle case" toggle button display text. + + + Title Case + TextEditor: ContextFlyout "Title case" toggle button display text. + + Untitled.txt TextEditor: Default file name for new document. - + Ln {0}, Col {1} ({2} {3}) TextEditor: LineColumnIndicator display text when character(s) is(are) selected. {0} stands for line number, {1} stands for column index, {2} stands for number of selected characters, {3} stands for "selected" (based on singular and plural). You can change the order but DO NOT REMOVE them from the string. - + Ln {0}, Col {1} TextEditor: LineColumnIndicator display text when no character is selected. {0} stands for line number, {1} stands for column index. You can change the order but DO NOT REMOVE them from the string. - + Preview text changes TextEditor: ModificationIndicator "PreviewTextChanges" MenuFlyoutItem display text. DiffViewer will be shown upon selection. - + Revert all changes TextEditor: ModificationIndicator "RevertAllChanges" MenuFlyoutItem display text. All changes including text, encoding and line ending will be reverted to original state upon selection. - + Modified TextEditor: ModificationIndicator display text. - + Copied TextEditor: Notification message when user tap or click file name/path on status bar (Bottom left corner). - + Saved TextEditor: Notification message when file has been saved successfully. - + Press F11 to exit full screen TextEditor: Notification message when app entering full screen mode. - + Reload file from disk TextEditor: FileModifiedOutsideIndicator "ReloadFileFromDisk" MenuFlyoutItem display text. - + File has been modified externally TextEditor: FileModifiedOutsideIndicator tool tip display text. - + File has been moved, renamed or deleted! TextEditor: FileRenamedMovedOrDeletedIndicator tool tip display text. - + File reloaded TextEditor: Notification message when file has been reloaded successfully. - + Compact Overlay App: "Compact Overlay" display text - + Full Screen App: "Full Screen" display text - + Exit Compact Overlay App: "Exit Compact Overlay" display text - + Exit Full Screen App: "Exit Full Screen" display text - + selected TextEditor: Plural form for the selected word count indicator. Leave it with the same value of SingularSelectedWord if your language doesen't have a plural form. - + selected TextEditor: Singular form for the selected word count indicator. - + Move tab here App: DragAndDrop UIOverride Caption: "Move tab here" display text - + Open with Notepads App: DragAndDrop UIOverride Caption: "Open with Notepads" display text - + This is a shadow window of Notepads. Session snapshot and settings are disabled. App: ShadowWindowIndicator Description display text. - + File already opened! TextEditor: Notification message when file has been opened in current app instance. - + New window JumpList: Windows Taskbar JumpList Items "New window" item display text. - + Opens a new window JumpList: Windows Taskbar JumpList Items "New window" item description display text. - + New Window MainMenu: "New Window" button display text. - + Open Recent MainMenu: "Open Recent" button display text. - + Go To Line GoTo: Go to bar placeholder text. - + Line number exceeds beyond the total number of lines! GoTo: Notification message when input exceeds input limit. - + You can only type a number! GoTo: Notification message when invalid input entered. - + Go To Line GoTo: "Search" button tool tip display text. - + Search in web TextEditor: ContextFlyout "Web Search" button display text. - + Restore Default Zoom TextEditor: FontZoomIndicator "Restore Default Zoom" FlyoutItem display text. Restores to default zoom for selected text editor. - + Zoom In TextEditor: FontZoomIndicator "ZoomIn" FlyoutItem display text. Zooms in selected text editor text. - + Zoom Out TextEditor: FontZoomIndicator "ZoomOut" FlyoutItem display text. Zooms out selected text editor text. - + Go To: GoTo: Go to bar label - + Print All... MainMenu: "Print All" button display text. - + Can only accept upto one decimal place Print: Error message when decimal places for margin entry exceeds limit. - + Value out of range Print: Error message when value for margin entry exceeds limit. - + Footer Print: PrintManager custom option "FooterText" title. - + Header Print: PrintManager custom option "HeaderText" title. - + Horizontal Margin (in %) Print: PrintManager custom option "LeftMargin" title. - + In % of paper width Print: PrintManager custom option "LeftMargin" and "TopMargin" description. - + Error printing: Print: Notification message when error occurs while showing print ui. - + Failed to print Print: Notification message on print failure. - + Printing is not supported on this device Print: Notification message when printing attempted in a non-supported device. - + Vertical Margin (in %) Print: PrintManager custom option "TopMargin" title. - + Use Regular Expression FindAndReplace: "Use Regular Expression" OptionToggleButton display text. - + Clear Recently Opened MainMenu: "Open Recent" button ClearRecentlyOpenedSubItem display text. - + More Encodings TextEditor: EncodingIndicator "More Encodings" FlyoutItem display text. - + Reopen with Encoding TextEditor: EncodingIndicator "Reopen with Encoding" FlyoutItem display text. - + Save with Encoding TextEditor: EncodingIndicator "Save with Encoding" FlyoutItem display text. - + Invalid regular expression! FindAndReplace: Notification message when regular expression text is invalid. - + Auto Guess Encoding TextEditor: EncodingIndicator "Auto Guess Encoding" FlyoutItem display text. - + Encoding cannot be determined TextEditor: Notification message when file's encoding cannot be determined. - + Find Previous (Shift+F3) FindAndReplace: "Find Previous" button tool tip display text. - + Toggle Replace Mode FindAndReplace: "Toggle Replace Mode" button tool tip display text. - + Right-to-Left Reading order TextEditor: ContextFlyout "Right-to-Left Reading order" toggle button display text. - + Italic FontStyle: "Italic" - + Normal FontStyle: "Normal" - + Oblique FontStyle: "Oblique" - + Black FontWeight: "Black" - + Bold FontWeight: "Bold" - + Extra Black FontWeight: "ExtraBlack" - + Extra Bold FontWeight: "ExtraBold" - + Extra Light FontWeight: "ExtraLight" - + Light FontWeight: "Light" - + Medium FontWeight: "Medium" - + Normal FontWeight: "Normal" - + Semi Bold FontWeight: "SemiBold" - + Semi Light FontWeight: "SemiLight" - + Thin FontWeight: "Thin" - + Cancel FileRenameDialog: CloseButtonText. - + Save FileRenameDialog: PrimaryButtonText. - + Rename FileRenameDialog: "Title" display text. - + File name should not contain invalid characters InvalidFilenameError: Filename contains invalid characters. - + File name should not contain leading spaces InvalidFilenameError: Filename contains leading spaces. - + File name should not contain trailing spaces InvalidFilenameError: Filename contains trailing spaces. - + File name cannot be empty or all whitespace InvalidFilenameError: Filename is empty or contains all whitespace. - + File name is invalid or not allowed InvalidFilenameError: Filename is invalid or not allowed. - + File name cannot be longer than 255 characters InvalidFilenameError: Filename is longer than 255 characters. - + Rename TextEditor: ContextFlyout "Rename" button display text. - + Renamed TextEditor: Notification message when file has been renamed successfully. - + Empty file extension is not supported at this moment FileRenameError: Empty file extension is not currently supported. - + File extension "{0}" is not supported at this moment FileRenameError: Extension is not currently supported. {0} stands for the file extension string. - + Close SessionCorruptionErrorDialog: CloseButtonText. - + Failed to recover data from the last session due to corrupted data. Please backup all your unsaved files (*.txt) in the session's folder. SessionCorruptionErrorDialog: "Content" display text. - + Open session backup folder SessionCorruptionErrorDialog: PrimaryButtonText. - + Warning SessionCorruptionErrorDialog: "Title" display text.