1
0

PaperWindow.xaml.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. using System.Numerics;
  2. using Content.Shared.Paper;
  3. using Robust.Client.AutoGenerated;
  4. using Robust.Client.Graphics;
  5. using Robust.Client.Input;
  6. using Robust.Client.ResourceManagement;
  7. using Robust.Client.UserInterface.CustomControls;
  8. using Robust.Client.UserInterface.Controls;
  9. using Robust.Client.UserInterface.XAML;
  10. using Robust.Shared.Utility;
  11. using Robust.Client.UserInterface.RichText;
  12. using Content.Client.UserInterface.RichText;
  13. using Robust.Shared.Input;
  14. namespace Content.Client.Paper.UI
  15. {
  16. [GenerateTypedNameReferences]
  17. public sealed partial class PaperWindow : BaseWindow
  18. {
  19. [Dependency] private readonly IInputManager _inputManager = default!;
  20. [Dependency] private readonly IResourceCache _resCache = default!;
  21. private static Color DefaultTextColor = new(25, 25, 25);
  22. // <summary>
  23. // Size of resize handles around the paper
  24. private const int DRAG_MARGIN_SIZE = 16;
  25. // We keep a reference to the paper content texture that we create
  26. // so that we can modify it later.
  27. private StyleBoxTexture _paperContentTex = new();
  28. // The number of lines that the content image represents.
  29. // See PaperVisualsComponent.ContentImageNumLines.
  30. private float _paperContentLineScale = 1.0f;
  31. // If paper limits the size in one or both axes, it'll affect whether
  32. // we're able to resize this UI or not. Default to everything enabled:
  33. private DragMode _allowedResizeModes = ~DragMode.None;
  34. private readonly Type[] _allowedTags = new Type[] {
  35. typeof(BoldItalicTag),
  36. typeof(BoldTag),
  37. typeof(BulletTag),
  38. typeof(ColorTag),
  39. typeof(HeadingTag),
  40. typeof(ItalicTag),
  41. typeof(MonoTag)
  42. };
  43. public event Action<string>? OnSaved;
  44. private int _MaxInputLength = -1;
  45. public int MaxInputLength
  46. {
  47. get
  48. {
  49. return _MaxInputLength;
  50. }
  51. set
  52. {
  53. _MaxInputLength = value;
  54. UpdateFillState();
  55. }
  56. }
  57. public PaperWindow()
  58. {
  59. IoCManager.InjectDependencies(this);
  60. RobustXamlLoader.Load(this);
  61. // We can't configure the RichTextLabel contents from xaml, so do it here:
  62. BlankPaperIndicator.SetMessage(Loc.GetString("paper-ui-blank-page-message"), null, DefaultTextColor);
  63. // Hook up the close button:
  64. CloseButton.OnPressed += _ => Close();
  65. Input.OnKeyBindDown += args => // Solution while TextEdit don't have events
  66. {
  67. if (args.Function == EngineKeyFunctions.MultilineTextSubmit)
  68. {
  69. // SaveButton is disabled when we hit the max input limit. Just check
  70. // that flag instead of trying to calculate the input length again
  71. if (!SaveButton.Disabled)
  72. {
  73. RunOnSaved();
  74. args.Handle();
  75. }
  76. }
  77. };
  78. Input.OnTextChanged += args =>
  79. {
  80. UpdateFillState();
  81. };
  82. SaveButton.OnPressed += _ =>
  83. {
  84. RunOnSaved();
  85. };
  86. SaveButton.Text = Loc.GetString("paper-ui-save-button",
  87. ("keybind", _inputManager.GetKeyFunctionButtonString(EngineKeyFunctions.MultilineTextSubmit)));
  88. }
  89. /// <summary>
  90. /// Initialize this UI according to <code>visuals</code> Initializes
  91. /// textures, recalculates sizes, and applies some layout rules.
  92. /// </summary>
  93. public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals)
  94. {
  95. // Randomize the placement of any stamps based on the entity UID
  96. // so that there's some variety in different papers.
  97. StampDisplay.PlacementSeed = (int)entity;
  98. // Initialize the background:
  99. PaperBackground.ModulateSelfOverride = visuals.BackgroundModulate;
  100. var backgroundImage = visuals.BackgroundImagePath != null? _resCache.GetResource<TextureResource>(visuals.BackgroundImagePath) : null;
  101. if (backgroundImage != null)
  102. {
  103. var backgroundImageMode = visuals.BackgroundImageTile ? StyleBoxTexture.StretchMode.Tile : StyleBoxTexture.StretchMode.Stretch;
  104. var backgroundPatchMargin = visuals.BackgroundPatchMargin;
  105. PaperBackground.PanelOverride = new StyleBoxTexture
  106. {
  107. Texture = backgroundImage,
  108. TextureScale = visuals.BackgroundScale,
  109. Mode = backgroundImageMode,
  110. PatchMarginLeft = backgroundPatchMargin.Left,
  111. PatchMarginBottom = backgroundPatchMargin.Bottom,
  112. PatchMarginRight = backgroundPatchMargin.Right,
  113. PatchMarginTop = backgroundPatchMargin.Top
  114. };
  115. }
  116. else
  117. {
  118. PaperBackground.PanelOverride = null;
  119. }
  120. // Then the header:
  121. if (visuals.HeaderImagePath != null)
  122. {
  123. HeaderImage.TexturePath = visuals.HeaderImagePath;
  124. HeaderImage.MinSize = HeaderImage.TextureNormal?.Size ?? Vector2.Zero;
  125. }
  126. HeaderImage.ModulateSelfOverride = visuals.HeaderImageModulate;
  127. HeaderImage.Margin = new Thickness(visuals.HeaderMargin.Left, visuals.HeaderMargin.Top,
  128. visuals.HeaderMargin.Right, visuals.HeaderMargin.Bottom);
  129. PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
  130. WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;
  131. FillStatus.ModulateSelfOverride = visuals.FontAccentColor;
  132. var contentImage = visuals.ContentImagePath != null ? _resCache.GetResource<TextureResource>(visuals.ContentImagePath) : null;
  133. if (contentImage != null)
  134. {
  135. // Setup the paper content texture, but keep a reference to it, as we can't set
  136. // some font-related properties here. We'll fix those up later, in Draw()
  137. _paperContentTex = new StyleBoxTexture
  138. {
  139. Texture = contentImage,
  140. Mode = StyleBoxTexture.StretchMode.Tile,
  141. };
  142. PaperContent.PanelOverride = _paperContentTex;
  143. _paperContentLineScale = visuals.ContentImageNumLines;
  144. }
  145. PaperContent.Margin = new Thickness(
  146. visuals.ContentMargin.Left, visuals.ContentMargin.Top,
  147. visuals.ContentMargin.Right, visuals.ContentMargin.Bottom);
  148. if (visuals.MaxWritableArea != null)
  149. {
  150. var a = (Vector2)visuals.MaxWritableArea;
  151. // Paper has requested that this has a maximum area that you can write on.
  152. // So, we'll make the window non-resizable and fix the size of the content.
  153. // Ideally, would like to be able to allow resizing only one direction.
  154. ScrollingContents.MinSize = Vector2.Zero;
  155. ScrollingContents.MinSize = a;
  156. if (a.X > 0.0f)
  157. {
  158. ScrollingContents.MaxWidth = a.X;
  159. _allowedResizeModes &= ~(DragMode.Left | DragMode.Right);
  160. // Since this dimension has been specified by the user, we
  161. // need to undo the SetSize which was configured in the xaml.
  162. // Controls use NaNs to indicate unset for this value.
  163. // This is leaky - there should be a method for this
  164. SetWidth = float.NaN;
  165. }
  166. if (a.Y > 0.0f)
  167. {
  168. ScrollingContents.MaxHeight = a.Y;
  169. _allowedResizeModes &= ~(DragMode.Top | DragMode.Bottom);
  170. SetHeight = float.NaN;
  171. }
  172. }
  173. }
  174. /// <summary>
  175. /// Control interface. We'll mostly rely on the children to do the drawing
  176. /// but in order to get lines on paper to match up with the rich text labels,
  177. /// we need to do a small calculation to sync them up.
  178. /// </summary>
  179. protected override void Draw(DrawingHandleScreen handle)
  180. {
  181. // Now do the deferred setup of the written area. At the point
  182. // that InitVisuals runs, the label hasn't had it's style initialized
  183. // so we need to get some info out now:
  184. if (WrittenTextLabel.TryGetStyleProperty<Font>("font", out var font))
  185. {
  186. float fontLineHeight = font.GetLineHeight(1.0f);
  187. // This positions the texture so the font baseline is on the bottom:
  188. _paperContentTex.ExpandMarginTop = font.GetDescent(UIScale);
  189. // And this scales the texture so that it's a single text line:
  190. var scaleY = (_paperContentLineScale * fontLineHeight) / _paperContentTex.Texture?.Height ?? fontLineHeight;
  191. _paperContentTex.TextureScale = new Vector2(1, scaleY);
  192. // Now, we might need to add some padding to the text to ensure
  193. // that, even if a header is specified, the text will line up with
  194. // where the content image expects the font to be rendered (i.e.,
  195. // adjusting the height of the header image shouldn't cause the
  196. // text to be offset from a line)
  197. {
  198. var headerHeight = HeaderImage.Size.Y + HeaderImage.Margin.Top + HeaderImage.Margin.Bottom;
  199. var headerInLines = headerHeight / (fontLineHeight * _paperContentLineScale);
  200. var paddingRequiredInLines = (float)Math.Ceiling(headerInLines) - headerInLines;
  201. var verticalMargin = fontLineHeight * paddingRequiredInLines * _paperContentLineScale;
  202. TextAlignmentPadding.Margin = new Thickness(0.0f, verticalMargin, 0.0f, 0.0f);
  203. }
  204. }
  205. base.Draw(handle);
  206. }
  207. /// <summary>
  208. /// Initialize the paper contents, i.e. the text typed by the
  209. /// user and any stamps that have peen put on the page.
  210. /// </summary>
  211. public void Populate(PaperComponent.PaperBoundUserInterfaceState state)
  212. {
  213. bool isEditing = state.Mode == PaperComponent.PaperAction.Write;
  214. bool wasEditing = InputContainer.Visible;
  215. InputContainer.Visible = isEditing;
  216. EditButtons.Visible = isEditing;
  217. var msg = new FormattedMessage();
  218. msg.AddMarkupPermissive(state.Text);
  219. // For premade documents, we want to be able to edit them rather than
  220. // replace them.
  221. var shouldCopyText = 0 == Input.TextLength && 0 != state.Text.Length;
  222. if (!wasEditing || shouldCopyText)
  223. {
  224. // We can get repeated messages with state.Mode == Write if another
  225. // player opens the UI for reading. In this case, don't update the
  226. // text input, as this player is currently writing new text and we
  227. // don't want to lose any text they already input.
  228. Input.TextRope = Rope.Leaf.Empty;
  229. Input.CursorPosition = new TextEdit.CursorPos();
  230. Input.InsertAtCursor(state.Text);
  231. }
  232. for (var i = 0; i <= state.StampedBy.Count * 3 + 1; i++)
  233. {
  234. msg.AddMarkupPermissive("\r\n");
  235. }
  236. WrittenTextLabel.SetMessage(msg, _allowedTags, DefaultTextColor);
  237. WrittenTextLabel.Visible = !isEditing && state.Text.Length > 0;
  238. BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
  239. StampDisplay.RemoveAllChildren();
  240. StampDisplay.RemoveStamps();
  241. foreach(var stamper in state.StampedBy)
  242. {
  243. StampDisplay.AddStamp(new StampWidget{ StampInfo = stamper });
  244. }
  245. }
  246. /// <summary>
  247. /// BaseWindow interface. Allow users to drag UI around by grabbing
  248. /// anywhere on the page (like FancyWindow) but try to calculate
  249. /// reasonable dragging bounds because this UI can have round corners,
  250. /// and it can be hard to judge where to click to resize.
  251. /// </summary>
  252. protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
  253. {
  254. var mode = DragMode.None;
  255. // Be quite generous with resize margins:
  256. if (relativeMousePos.Y < DRAG_MARGIN_SIZE)
  257. {
  258. mode |= DragMode.Top;
  259. }
  260. else if (relativeMousePos.Y > Size.Y - DRAG_MARGIN_SIZE)
  261. {
  262. mode |= DragMode.Bottom;
  263. }
  264. if (relativeMousePos.X < DRAG_MARGIN_SIZE)
  265. {
  266. mode |= DragMode.Left;
  267. }
  268. else if (relativeMousePos.X > Size.X - DRAG_MARGIN_SIZE)
  269. {
  270. mode |= DragMode.Right;
  271. }
  272. if((mode & _allowedResizeModes) == DragMode.None)
  273. {
  274. return DragMode.Move;
  275. }
  276. return mode & _allowedResizeModes;
  277. }
  278. private void RunOnSaved()
  279. {
  280. // Prevent further saving while text processing still in
  281. SaveButton.Disabled = true;
  282. OnSaved?.Invoke(Rope.Collapse(Input.TextRope));
  283. }
  284. private void UpdateFillState()
  285. {
  286. if (MaxInputLength != -1)
  287. {
  288. var inputLength = Input.TextLength;
  289. FillStatus.Text = Loc.GetString("paper-ui-fill-level",
  290. ("currentLength", inputLength),
  291. ("maxLength", MaxInputLength));
  292. // Disable the save button if we've gone over the limit
  293. SaveButton.Disabled = inputLength > MaxInputLength;
  294. }
  295. else
  296. {
  297. FillStatus.Text = "";
  298. SaveButton.Disabled = false;
  299. }
  300. }
  301. }
  302. }