Client LuaCsForBarotrauma
SpriteEditorScreen.cs
1 using Microsoft.Xna.Framework;
2 using Microsoft.Xna.Framework.Graphics;
3 using Microsoft.Xna.Framework.Input;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Xml.Linq;
9 #if DEBUG
10 using System.IO;
11 #else
12 using Barotrauma.IO;
13 #endif
14 
15 namespace Barotrauma
16 {
18  {
19  private GUIListBox textureList, spriteList;
20 
21  private GUIFrame topPanel;
22  private GUIFrame leftPanel;
23  private GUIFrame rightPanel;
24  private GUIFrame bottomPanel;
25  private GUIFrame backgroundColorPanel;
26 
27  private bool drawGrid, snapToGrid;
28 
29  private GUIFrame topPanelContents;
30  private GUITextBlock texturePathText;
31  private GUITextBlock xmlPathText;
32  private GUIScrollBar zoomBar;
33  private readonly List<Sprite> selectedSprites = new List<Sprite>();
34  private readonly List<Sprite> dirtySprites = new List<Sprite>();
35  private Texture2D SelectedTexture => lastSprite?.Texture;
36  private Sprite lastSprite;
37  private string selectedTexturePath;
38 
39  private Rectangle textureRect;
40  private float zoom = 1;
41  private const float MinZoom = 0.25f, MaxZoom = 10.0f;
42 
43  private GUITextBox filterSpritesBox;
44  private GUITextBlock filterSpritesLabel;
45  private GUITextBox filterTexturesBox;
46  private GUITextBlock filterTexturesLabel;
47 
48  private LocalizedString originLabel, positionLabel, sizeLabel;
49 
50  private bool editBackgroundColor;
51  private Color backgroundColor = new Color(0.051f, 0.149f, 0.271f, 1.0f);
52 
53  private bool ControlDown => PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl);
54 
55  private readonly Camera cam;
56  public override Camera Cam
57  {
58  get { return cam; }
59  }
60 
62  {
63  get { return topPanel; }
64  }
65 
67  {
68  cam = new Camera();
70  CreateUI();
71  }
72 
73  #region Initialization
74  private void CreateUI()
75  {
76  originLabel = TextManager.Get("charactereditor.origin");
77  positionLabel = TextManager.GetWithVariable("charactereditor.position", "[coordinates]", string.Empty);
78  sizeLabel = TextManager.Get("charactereditor.size");
79 
80  topPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), Frame.RectTransform) { MinSize = new Point(0, 60) }, "GUIFrameTop");
81  topPanelContents = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.8f), topPanel.RectTransform, Anchor.Center), style: null);
82 
83  new GUIButton(new RectTransform(new Vector2(0.14f, 0.4f), topPanelContents.RectTransform, Anchor.TopLeft)
84  {
85  RelativeOffset = new Vector2(0, 0.1f)
86  }, TextManager.Get("spriteeditor.reloadtexture"))
87  {
88  OnClicked = (button, userData) =>
89  {
90  var selected = selectedSprites.ToList();
91  Sprite firstSelected = selected.First();
92  selected.ForEach(s => s.ReloadTexture());
93  RefreshLists();
94  textureList.Select(firstSelected.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled);
95  selected.ForEachMod(s => spriteList.Select(s, autoScroll: GUIListBox.AutoScroll.Disabled));
96  texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value);
97  texturePathText.TextColor = GUIStyle.Green;
98  return true;
99  }
100  };
101  new GUIButton(new RectTransform(new Vector2(0.14f, 0.4f), topPanelContents.RectTransform, Anchor.BottomLeft)
102  {
103  RelativeOffset = new Vector2(0, 0.1f)
104  }, TextManager.Get("spriteeditor.resetchanges"))
105  {
106  OnClicked = (button, userData) =>
107  {
108  if (SelectedTexture == null) { return false; }
109  foreach (Sprite sprite in loadedSprites)
110  {
111  if (sprite.FullPath != selectedTexturePath) { continue; }
112  var element = sprite.SourceElement;
113  if (element == null) { continue; }
114  // Not all sprites have a sourcerect defined, in which case we'll want to use the current source rect instead of an empty rect.
115  sprite.SourceRect = element.GetAttributeRect("sourcerect", sprite.SourceRect);
116  sprite.RelativeOrigin = element.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f));
117  }
118  ResetWidgets();
119  xmlPathText.Text = TextManager.Get("spriteeditor.resetsuccessful");
120  xmlPathText.TextColor = GUIStyle.Green;
121  return true;
122  }
123  };
124  new GUIButton(new RectTransform(new Vector2(0.14f, 0.4f), topPanelContents.RectTransform, Anchor.TopLeft)
125  {
126  RelativeOffset = new Vector2(0.15f, 0.1f)
127  }, TextManager.Get("spriteeditor.saveselectedsprites"))
128  {
129  OnClicked = (button, userData) =>
130  {
131  return SaveSprites(selectedSprites);
132  }
133  };
134  new GUIButton(new RectTransform(new Vector2(0.14f, 0.4f), topPanelContents.RectTransform, Anchor.BottomLeft)
135  {
136  RelativeOffset = new Vector2(0.15f, 0.1f)
137  }, TextManager.Get("spriteeditor.saveallsprites"))
138  {
139  OnClicked = (button, userData) =>
140  {
141  return SaveSprites(loadedSprites);
142  }
143  };
144 
145  GUITextBlock.AutoScaleAndNormalize(topPanelContents.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock));
146 
147  new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.2f), topPanelContents.RectTransform, Anchor.TopCenter, Pivot.CenterRight) { RelativeOffset = new Vector2(0, 0.3f) }, TextManager.Get("spriteeditor.zoom"));
148  zoomBar = new GUIScrollBar(new RectTransform(new Vector2(0.2f, 0.35f), topPanelContents.RectTransform, Anchor.TopCenter, Pivot.CenterRight)
149  {
150  RelativeOffset = new Vector2(0.05f, 0.3f)
151  }, style: "GUISlider", barSize: 0.1f)
152  {
153  BarScroll = GetBarScrollValue(),
154  Step = 0.01f,
155  OnMoved = (scrollBar, value) =>
156  {
157  zoom = MathHelper.Lerp(MinZoom, MaxZoom, value);
158  viewAreaOffset = Point.Zero;
159  return true;
160  }
161  };
162  var resetBtn = new GUIButton(new RectTransform(new Vector2(0.05f, 0.35f), topPanelContents.RectTransform, Anchor.TopCenter, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.055f, 0.3f) }, TextManager.Get("spriteeditor.resetzoom"))
163  {
164  OnClicked = (box, data) =>
165  {
166  ResetZoom();
167  return true;
168  }
169  };
170  resetBtn.TextBlock.AutoScaleHorizontal = true;
171 
172  new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), topPanelContents.RectTransform, Anchor.BottomCenter, Pivot.CenterRight) { RelativeOffset = new Vector2(0, 0.3f) }, TextManager.Get("spriteeditor.showgrid"))
173  {
174  Selected = drawGrid,
175  OnSelected = (tickBox) =>
176  {
177  drawGrid = tickBox.Selected;
178  return true;
179  }
180  };
181  new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), topPanelContents.RectTransform, Anchor.BottomCenter, Pivot.CenterRight) { RelativeOffset = new Vector2(0.17f, 0.3f) }, TextManager.Get("spriteeditor.snaptogrid"))
182  {
183  Selected = snapToGrid,
184  OnSelected = (tickBox) =>
185  {
186  snapToGrid = tickBox.Selected;
187  return true;
188  }
189  };
190 
191  texturePathText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.4f), topPanelContents.RectTransform, Anchor.Center, Pivot.BottomCenter) { RelativeOffset = new Vector2(0.4f, 0) }, "", Color.LightGray);
192  xmlPathText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.4f), topPanelContents.RectTransform, Anchor.Center, Pivot.TopCenter) { RelativeOffset = new Vector2(0.4f, 0) }, "", Color.LightGray);
193 
194  leftPanel = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f - topPanel.RectTransform.RelativeSize.Y), Frame.RectTransform, Anchor.BottomLeft)
195  { MinSize = new Point(150, 0) }, style: "GUIFrameLeft");
196  var paddedLeftPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), leftPanel.RectTransform, Anchor.Center))
197  { RelativeSpacing = 0.01f, Stretch = true };
198 
199  var filterArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), paddedLeftPanel.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true)
200  {
201  Stretch = true,
202  UserData = "filterarea"
203  };
204  filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; ;
205  filterTexturesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font, createClearButton: true);
206  filterArea.RectTransform.MinSize = filterTexturesBox.RectTransform.MinSize;
207  filterTexturesBox.OnTextChanged += (textBox, text) => { FilterTextures(text); return true; };
208 
209  textureList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLeftPanel.RectTransform))
210  {
211  PlaySoundOnSelect = true,
212  OnSelected = (listBox, userData) =>
213  {
214  var newTexturePath = userData as string;
215  if (selectedTexturePath == null || selectedTexturePath != newTexturePath)
216  {
217  selectedTexturePath = newTexturePath;
218  ResetZoom();
219  spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: GUIListBox.AutoScroll.Disabled);
220  UpdateScrollBar(spriteList);
221  }
222  foreach (GUIComponent child in spriteList.Content.Children)
223  {
224  var textBlock = (GUITextBlock)child;
225  var sprite = (Sprite)textBlock.UserData;
226  textBlock.TextColor = new Color(textBlock.TextColor, sprite.FilePath == selectedTexturePath ? 1.0f : 0.4f);
227  if (sprite.FilePath == selectedTexturePath) { textBlock.Visible = true; }
228  }
229  texturePathText.TextColor = Color.LightGray;
230  topPanelContents.Visible = true;
231  return true;
232  }
233  };
234 
235  rightPanel = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f - topPanel.RectTransform.RelativeSize.Y), Frame.RectTransform, Anchor.BottomRight) { MinSize = new Point(150, 0) }, style: "GUIFrameRight");
236  var paddedRightPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), rightPanel.RectTransform, Anchor.Center))
237  {
238  Stretch = true,
239  RelativeSpacing = 0.01f
240  };
241 
242  filterArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), paddedRightPanel.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true)
243  {
244  Stretch = true,
245  UserData = "filterarea"
246  };
247  filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true };
248  filterSpritesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font, createClearButton: true);
249  filterArea.RectTransform.MinSize = filterSpritesBox.RectTransform.MinSize;
250  filterSpritesBox.OnTextChanged += (textBox, text) => { FilterSprites(text); return true; };
251 
252  spriteList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform))
253  {
254  PlaySoundOnSelect = true,
255  OnSelected = (listBox, userData) =>
256  {
257  if (userData is Sprite sprite)
258  {
259  SelectSprite(sprite);
260  return true;
261  }
262  return false;
263  }
264  };
265 
266  // Background color
267  bottomPanel = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.05f), Frame.RectTransform, Anchor.BottomCenter), style: null, color: Color.Black * 0.5f);
268  new GUITickBox(new RectTransform(new Vector2(0.2f, 0.5f), bottomPanel.RectTransform, Anchor.Center), TextManager.Get("charactereditor.editbackgroundcolor"))
269  {
270  Selected = editBackgroundColor,
271  OnSelected = box =>
272  {
273  editBackgroundColor = box.Selected;
274  return true;
275  }
276  };
277  backgroundColorPanel = new GUIFrame(new RectTransform(new Point(400, 80), Frame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0, 0.1f) }, style: null, color: Color.Black * 0.4f);
278  new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), backgroundColorPanel.RectTransform) { MinSize = new Point(80, 26) }, TextManager.Get("spriteeditor.backgroundcolor"), textColor: Color.WhiteSmoke);
279  var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1), backgroundColorPanel.RectTransform, Anchor.TopRight)
280  {
281  AbsoluteOffset = new Point(20, 0)
282  }, isHorizontal: true, childAnchor: Anchor.CenterRight)
283  {
284  Stretch = true,
285  RelativeSpacing = 0.01f
286  };
287  var fields = new GUIComponent[4];
288  LocalizedString[] colorComponentLabels = { TextManager.Get("spriteeditor.colorcomponentr"), TextManager.Get("spriteeditor.colorcomponentg"), TextManager.Get("spriteeditor.colorcomponentb") };
289  for (int i = 2; i >= 0; i--)
290  {
291  var element = new GUIFrame(new RectTransform(new Vector2(0.2f, 1), inputArea.RectTransform)
292  {
293  MinSize = new Point(40, 0),
294  MaxSize = new Point(100, 50)
295  }, style: null, color: Color.Black * 0.6f);
296  var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i],
297  font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft);
298  var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int)
299  {
300  Font = GUIStyle.SmallFont
301  };
302  numberInput.MinValueInt = 0;
303  numberInput.MaxValueInt = 255;
304  numberInput.Font = GUIStyle.SmallFont;
305  switch (i)
306  {
307  case 0:
308  colorLabel.TextColor = GUIStyle.Red;
309  numberInput.IntValue = backgroundColor.R;
310  numberInput.OnValueChanged += (numInput) => backgroundColor.R = (byte)(numInput.IntValue);
311  break;
312  case 1:
313  colorLabel.TextColor = GUIStyle.Green;
314  numberInput.IntValue = backgroundColor.G;
315  numberInput.OnValueChanged += (numInput) => backgroundColor.G = (byte)(numInput.IntValue);
316  break;
317  case 2:
318  colorLabel.TextColor = Color.DeepSkyBlue;
319  numberInput.IntValue = backgroundColor.B;
320  numberInput.OnValueChanged += (numInput) => backgroundColor.B = (byte)(numInput.IntValue);
321  break;
322  }
323  }
324  }
325 
326  private readonly HashSet<Sprite> loadedSprites = new HashSet<Sprite>();
327  private void LoadSprites()
328  {
329  loadedSprites.ForEach(s => s.Remove());
330  loadedSprites.Clear();
331  var contentPackages = ContentPackageManager.EnabledPackages.All.ToList();
332 
333 #if !DEBUG
334  var vanilla = GameMain.VanillaContent;
335  if (vanilla != null)
336  {
337  contentPackages.Remove(vanilla);
338  }
339 #endif
340  foreach (var contentPackage in contentPackages)
341  {
342  foreach (var file in contentPackage.Files)
343  {
344  if (file.Path.EndsWith(".xml"))
345  {
346  XDocument doc = XMLExtensions.TryLoadXml(file.Path);
347  if (doc != null)
348  {
349  LoadSprites(doc.Root.FromPackage(file.Path.ContentPackage));
350  }
351  }
352  }
353  }
354 
355  void LoadSprites(ContentXElement element)
356  {
357  string[] spriteElementNames = {
358  "Sprite",
359  "DeformableSprite",
360  "BackgroundSprite",
361  "BrokenSprite",
362  "ContainedSprite",
363  "InventoryIcon",
364  "Icon",
365  "VineSprite",
366  "LeafSprite",
367  "FlowerSprite",
368  "DecorativeSprite",
369  "BarrelSprite",
370  "RailSprite",
371  "SchematicSprite",
372  "WeldedSprite"
373  };
374 
375  foreach (string spriteElementName in spriteElementNames)
376  {
377  element.GetChildElements(spriteElementName).ForEach(s => CreateSprite(s));
378  }
379 
380  element.Elements().ForEach(e => LoadSprites(e));
381  }
382 
383  void CreateSprite(ContentXElement element)
384  {
385  //empty element, probably an item variant?
386  if (element.Attributes().None()) { return; }
387 
388  string spriteFolder = "";
389  ContentPath texturePath = null;
390 
391  if (element.GetAttribute("texture") != null)
392  {
393  texturePath = element.GetAttributeContentPath("texture");
394  }
395  else
396  {
397  if (element.Name.ToString().ToLower() == "vinesprite")
398  {
399  texturePath = element.Parent.GetAttributeContentPath("vineatlas");
400  }
401  }
402  if (texturePath.IsNullOrEmpty()) { return; }
403 
404  // TODO: parse and create?
405  if (texturePath.Value.Contains("[GENDER]") || texturePath.Value.Contains("[HEADID]") || texturePath.Value.Contains("[RACE]") || texturePath.Value.Contains("[VARIANT]")) { return; }
406  if (!texturePath.Value.Contains("/"))
407  {
408  var parsedPath = element.ParseContentPathFromUri();
409  spriteFolder = Path.GetDirectoryName(parsedPath);
410  }
411  // Uncomment if we do multiple passes -> there can be duplicates
412  //string identifier = Sprite.GetID(element);
413  //if (loadedSprites.None(s => s.ID == identifier))
414  //{
415  // loadedSprites.Add(new Sprite(element, spriteFolder));
416  //}
417  loadedSprites.Add(new Sprite(element, spriteFolder, texturePath.Value, lazyLoad: true));
418  }
419  }
420 
421  private bool SaveSprites(IEnumerable<Sprite> sprites)
422  {
423  if (SelectedTexture == null) { return false; }
424  if (sprites.None()) { return false; }
425  HashSet<XDocument> docsToSave = new HashSet<XDocument>();
426  foreach (Sprite sprite in sprites)
427  {
428  if (sprite.FullPath != selectedTexturePath) { continue; }
429  var element = sprite.SourceElement;
430  if (element == null) { continue; }
431  element.SetAttributeValue("sourcerect", XMLExtensions.RectToString(sprite.SourceRect));
432  element.SetAttributeValue("origin", XMLExtensions.Vector2ToString(sprite.RelativeOrigin));
433 
434  /*if (element.Attribute("slice") != null)
435  {
436  Rectangle slice = new Rectangle(
437  sprite.SourceRect.X + 5,
438  sprite.SourceRect.Y + 5,
439  sprite.SourceRect.Right - 5,
440  sprite.SourceRect.Bottom - 5);
441  element.SetAttributeValue("slice", XMLExtensions.RectToString(slice));
442  }*/
443  docsToSave.Add(element.Document);
444  }
445  xmlPathText.Text = TextManager.Get("spriteeditor.allchangessavedto");
446  foreach (XDocument doc in docsToSave)
447  {
448  string xmlPath = doc.ParseContentPathFromUri();
449  xmlPathText.Text += "\n" + xmlPath;
450 #if DEBUG
451  doc.Save(xmlPath);
452 #else
453  doc.SaveSafe(xmlPath);
454 #endif
455  }
456  xmlPathText.TextColor = GUIStyle.Green;
457  return true;
458  }
459 #endregion
460 
461  #region Public methods
462  public override void AddToGUIUpdateList()
463  {
464  leftPanel.AddToGUIUpdateList();
465  rightPanel.AddToGUIUpdateList();
466  topPanel.AddToGUIUpdateList();
467  bottomPanel.AddToGUIUpdateList();
468  if (editBackgroundColor)
469  {
470  backgroundColorPanel.AddToGUIUpdateList();
471  }
472  }
473 
474  public override void Update(double deltaTime)
475  {
476  base.Update(deltaTime);
477  Widget.EnableMultiSelect = ControlDown;
478  spriteList.SelectMultiple = Widget.EnableMultiSelect;
479  // Select rects with the mouse
480  if (Widget.SelectedWidgets.None() || Widget.EnableMultiSelect)
481  {
482  if (SelectedTexture != null && GUI.MouseOn == null)
483  {
484  foreach (Sprite sprite in loadedSprites)
485  {
486  if (sprite.FullPath != selectedTexturePath) { continue; }
488  {
489  var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom));
490  if (scaledRect.Contains(PlayerInput.MousePosition))
491  {
492  spriteList.Select(sprite, autoScroll: GUIListBox.AutoScroll.Disabled);
493  UpdateScrollBar(spriteList);
494  UpdateScrollBar(textureList);
495  // Release the keyboard so that we can nudge the source rects
496  GUI.KeyboardDispatcher.Subscriber = null;
497  }
498  }
499  }
500  }
501  }
502  if (GUI.MouseOn == null)
503  {
504  if (PlayerInput.ScrollWheelSpeed != 0)
505  {
506  zoom = MathHelper.Clamp(zoom + PlayerInput.ScrollWheelSpeed * (float)deltaTime * 0.05f * zoom, MinZoom, MaxZoom);
507  zoomBar.BarScroll = GetBarScrollValue();
508  }
509  widgets.Values.ForEach(w => w.Update((float)deltaTime));
511  {
512  // "Camera" Pan
513  Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 100.0f;
514  viewAreaOffset += moveSpeed.ToPoint();
515  }
516  }
517  if (GUI.KeyboardDispatcher.Subscriber == null)
518  {
519  if (PlayerInput.KeyHit(Keys.Left))
520  {
521  Nudge(Keys.Left);
522  }
523  if (PlayerInput.KeyHit(Keys.Right))
524  {
525  Nudge(Keys.Right);
526  }
527  if (PlayerInput.KeyHit(Keys.Down))
528  {
529  Nudge(Keys.Down);
530  }
531  if (PlayerInput.KeyHit(Keys.Up))
532  {
533  Nudge(Keys.Up);
534  }
535  if (PlayerInput.KeyDown(Keys.Left))
536  {
537  holdTimer += deltaTime;
538  if (holdTimer > holdTime)
539  {
540  Nudge(Keys.Left);
541  }
542  }
543  else if (PlayerInput.KeyDown(Keys.Right))
544  {
545  holdTimer += deltaTime;
546  if (holdTimer > holdTime)
547  {
548  Nudge(Keys.Right);
549  }
550  }
551  else if (PlayerInput.KeyDown(Keys.Down))
552  {
553  holdTimer += deltaTime;
554  if (holdTimer > holdTime)
555  {
556  Nudge(Keys.Down);
557  }
558  }
559  else if (PlayerInput.KeyDown(Keys.Up))
560  {
561  holdTimer += deltaTime;
562  if (holdTimer > holdTime)
563  {
564  Nudge(Keys.Up);
565  }
566  }
567  else
568  {
569  holdTimer = 0;
570  }
571  }
572  }
573 
574  private double holdTimer;
575  private readonly float holdTime = 0.2f;
576  private void Nudge(Keys key)
577  {
578  switch (key)
579  {
580  case Keys.Left:
581  foreach (var sprite in selectedSprites)
582  {
583  var newRect = sprite.SourceRect;
584  if (ControlDown)
585  {
586  newRect.Width--;
587  }
588  else
589  {
590  newRect.X--;
591  }
592  UpdateSourceRect(sprite, newRect);
593  }
594  break;
595  case Keys.Right:
596  foreach (var sprite in selectedSprites)
597  {
598  var newRect = sprite.SourceRect;
599  if (ControlDown)
600  {
601  newRect.Width++;
602  }
603  else
604  {
605  newRect.X++;
606  }
607  UpdateSourceRect(sprite, newRect);
608  }
609  break;
610  case Keys.Down:
611  foreach (var sprite in selectedSprites)
612  {
613  var newRect = sprite.SourceRect;
614  if (ControlDown)
615  {
616  newRect.Height++;
617  }
618  else
619  {
620  newRect.Y++;
621  }
622  UpdateSourceRect(sprite, newRect);
623  }
624  break;
625  case Keys.Up:
626  foreach (var sprite in selectedSprites)
627  {
628  var newRect = sprite.SourceRect;
629  if (ControlDown)
630  {
631  newRect.Height--;
632  }
633  else
634  {
635  newRect.Y--;
636  }
637  UpdateSourceRect(sprite, newRect);
638  }
639  break;
640  }
641  }
642 
643 
644  public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
645  {
646  graphics.Clear(backgroundColor);
647  spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable, samplerState: SamplerState.PointClamp);
648 
649  var viewArea = GetViewArea;
650 
651  if (SelectedTexture != null)
652  {
653  textureRect = new Rectangle(
654  (int)(viewArea.Center.X - SelectedTexture.Bounds.Width / 2f * zoom),
655  (int)(viewArea.Center.Y - SelectedTexture.Bounds.Height / 2f * zoom),
656  (int)(SelectedTexture.Bounds.Width * zoom),
657  (int)(SelectedTexture.Bounds.Height * zoom));
658 
659  spriteBatch.Draw(SelectedTexture,
660  viewArea.Center.ToVector2(),
661  sourceRectangle: null,
662  color: Color.White,
663  rotation: 0.0f,
664  origin: new Vector2(SelectedTexture.Bounds.Width / 2.0f, SelectedTexture.Bounds.Height / 2.0f),
665  scale: zoom,
666  effects: SpriteEffects.None,
667  layerDepth: 0);
668 
669  //GUI.DrawRectangle(spriteBatch, viewArea, Color.Green, isFilled: false);
670  GUI.DrawRectangle(spriteBatch, textureRect, Color.Gray, isFilled: false);
671 
672  if (drawGrid)
673  {
674  DrawGrid(spriteBatch, textureRect, zoom, Submarine.GridSize);
675  }
676 
677  foreach (GUIComponent element in spriteList.Content.Children)
678  {
679  if (!(element.UserData is Sprite sprite)) { continue; }
680  if (sprite.FullPath != selectedTexturePath) { continue; }
681 
682  Rectangle sourceRect = new Rectangle(
683  textureRect.X + (int)(sprite.SourceRect.X * zoom),
684  textureRect.Y + (int)(sprite.SourceRect.Y * zoom),
685  (int)(sprite.SourceRect.Width * zoom),
686  (int)(sprite.SourceRect.Height * zoom));
687 
688  bool isSelected = selectedSprites.Contains(sprite);
689  GUI.DrawRectangle(spriteBatch, sourceRect, isSelected ? GUIStyle.Orange : GUIStyle.Red * 0.5f, thickness: isSelected ? 2 : 1);
690 
691  Identifier id = sprite.Identifier;
692  if (!id.IsEmpty)
693  {
694  int widgetSize = 10;
695  Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2();
696  Vector2 GetTopRight() => new Vector2(GetTopLeft().X + sprite.SourceRect.Width, GetTopLeft().Y);
697  Vector2 GetBottomRight() => new Vector2(GetTopRight().X, GetTopRight().Y + sprite.SourceRect.Height);
698  var originWidget = GetWidget($"{id}_origin", sprite, widgetSize, WidgetShape.Cross, initMethod: w =>
699  {
700  w.Tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal());
701  w.MouseHeld += dTime =>
702  {
703  w.DrawPos = PlayerInput.MousePosition.Clamp(textureRect.Location.ToVector2() + GetTopLeft() * zoom, textureRect.Location.ToVector2() + GetBottomRight() * zoom);
704  sprite.Origin = (w.DrawPos - textureRect.Location.ToVector2() - sprite.SourceRect.Location.ToVector2() * zoom) / zoom;
705  w.Tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal());
706  };
707  w.Refresh = () =>
708  w.DrawPos = (textureRect.Location.ToVector2() + (sprite.Origin + sprite.SourceRect.Location.ToVector2()) * zoom)
709  .Clamp(textureRect.Location.ToVector2() + GetTopLeft() * zoom, textureRect.Location.ToVector2() + GetBottomRight() * zoom);
710  });
711  var positionWidget = GetWidget($"{id}_position", sprite, widgetSize, WidgetShape.Rectangle, initMethod: w =>
712  {
713  w.Tooltip = positionLabel + sprite.SourceRect.Location;
714  w.MouseHeld += dTime =>
715  {
716  w.DrawPos = (drawGrid && snapToGrid) ?
717  SnapToGrid(PlayerInput.MousePosition, textureRect, zoom, Submarine.GridSize, Submarine.GridSize.X / 4.0f * zoom) :
718  PlayerInput.MousePosition;
719  w.DrawPos = new Vector2((float)Math.Ceiling(w.DrawPos.X), (float)Math.Ceiling(w.DrawPos.Y));
720  sprite.SourceRect = new Rectangle(((w.DrawPos - textureRect.Location.ToVector2()) / zoom).ToPoint(), sprite.SourceRect.Size);
721  if (spriteList.SelectedComponent is GUITextBlock textBox)
722  {
723  // TODO: cache the sprite name?
724  textBox.Text = GetSpriteName(sprite) + " " + sprite.SourceRect;
725  }
726  w.Tooltip = positionLabel + sprite.SourceRect.Location;
727  };
728  w.Refresh = () => w.DrawPos = textureRect.Location.ToVector2() + sprite.SourceRect.Location.ToVector2() * zoom;
729  });
730  var sizeWidget = GetWidget($"{id}_size", sprite, widgetSize, WidgetShape.Rectangle, initMethod: w =>
731  {
732  w.Tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString());
733  w.MouseHeld += dTime =>
734  {
735  w.DrawPos = (drawGrid && snapToGrid) ?
736  SnapToGrid(PlayerInput.MousePosition, textureRect, zoom, Submarine.GridSize, Submarine.GridSize.X / 4.0f * zoom) :
737  PlayerInput.MousePosition;
738  w.DrawPos = new Vector2((float)Math.Ceiling(w.DrawPos.X), (float)Math.Ceiling(w.DrawPos.Y));
739  sprite.SourceRect = new Rectangle(sprite.SourceRect.Location, ((w.DrawPos - positionWidget.DrawPos) / zoom).ToPoint());
740  // TODO: allow to lock the origin
741  sprite.RelativeOrigin = sprite.RelativeOrigin;
742  if (spriteList.SelectedComponent is GUITextBlock textBox)
743  {
744  // TODO: cache the sprite name?
745  textBox.Text = GetSpriteName(sprite) + " " + sprite.SourceRect;
746  }
747  w.Tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString());
748  };
749  w.Refresh = () => w.DrawPos = textureRect.Location.ToVector2() + new Vector2(sprite.SourceRect.Right, sprite.SourceRect.Bottom) * zoom;
750  });
751  originWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null;
752  positionWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null;
753  sizeWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null;
754  if (isSelected)
755  {
756  positionWidget.Draw(spriteBatch, (float)deltaTime);
757  sizeWidget.Draw(spriteBatch, (float)deltaTime);
758  originWidget.Draw(spriteBatch, (float)deltaTime);
759  }
760  }
761  }
762  }
763 
764  GUI.Draw(Cam, spriteBatch);
765 
766  spriteBatch.End();
767  }
768 
769  private void DrawGrid(SpriteBatch spriteBatch, Rectangle gridArea, float zoom, Vector2 gridSize)
770  {
771  gridSize *= zoom;
772  if (gridSize.X < 1.0f) { return; }
773  if (gridSize.Y < 1.0f) { return; }
774  int xLines = (int)(gridArea.Width / gridSize.X);
775  int yLines = (int)(gridArea.Height / gridSize.Y);
776 
777  for (int x = 0; x <= xLines; x++)
778  {
779  GUI.DrawLine(spriteBatch,
780  new Vector2(gridArea.X + x * gridSize.X, gridArea.Y),
781  new Vector2(gridArea.X + x * gridSize.X, gridArea.Bottom),
782  Color.White * 0.25f);
783  }
784  for (int y = 0; y <= yLines; y++)
785  {
786  GUI.DrawLine(spriteBatch,
787  new Vector2(gridArea.X, gridArea.Y + y * gridSize.Y),
788  new Vector2(gridArea.Right, gridArea.Y + y * gridSize.Y),
789  Color.White * 0.25f);
790  }
791  }
792 
793  private Vector2 SnapToGrid(Vector2 position, Rectangle gridArea, float zoom, Vector2 gridSize, float tolerance)
794  {
795  gridSize *= zoom;
796  if (gridSize.X < 1.0f) { return position; }
797  if (gridSize.Y < 1.0f) { return position; }
798 
799  Vector2 snappedPos = position;
800  snappedPos.X -= gridArea.X;
801  snappedPos.Y -= gridArea.Y;
802 
803  Vector2 gridPos = new Vector2(
804  MathUtils.RoundTowardsClosest(snappedPos.X, gridSize.X),
805  MathUtils.RoundTowardsClosest(snappedPos.Y, gridSize.Y));
806 
807  if (Math.Abs(gridPos.X - snappedPos.X) < tolerance)
808  {
809  snappedPos.X = gridPos.X;
810  }
811  if (Math.Abs(gridPos.Y - snappedPos.Y) < tolerance)
812  {
813  snappedPos.Y = gridPos.Y;
814  }
815 
816  snappedPos.X += gridArea.X;
817  snappedPos.Y += gridArea.Y;
818  return snappedPos;
819  }
820 
821  private void FilterTextures(string text)
822  {
823  if (string.IsNullOrWhiteSpace(text))
824  {
825  filterTexturesLabel.Visible = true;
826  textureList.Content.Children.ForEach(c => c.Visible = true);
827  return;
828  }
829  text = text.ToLower();
830  filterTexturesLabel.Visible = false;
831  foreach (GUIComponent child in textureList.Content.Children)
832  {
833  if (!(child is GUITextBlock textBlock)) { continue; }
834  textBlock.Visible = textBlock.Text.Contains(text, StringComparison.OrdinalIgnoreCase);
835  }
836  }
837  private void FilterSprites(string text)
838  {
839  if (string.IsNullOrWhiteSpace(text))
840  {
841  filterSpritesLabel.Visible = true;
842  spriteList.Content.Children.ForEach(c => c.Visible = true);
843  return;
844  }
845  text = text.ToLower();
846  filterSpritesLabel.Visible = false;
847  foreach (GUIComponent child in spriteList.Content.Children)
848  {
849  if (!(child is GUITextBlock textBlock)) { continue; }
850  textBlock.Visible = textBlock.Text.Contains(text, StringComparison.OrdinalIgnoreCase);
851  }
852  }
853 
854  public override void Select()
855  {
856  base.Select();
857  LoadSprites();
858  RefreshLists();
859  spriteList.Select(0, autoScroll: GUIListBox.AutoScroll.Disabled);
860  }
861 
862  protected override void DeselectEditorSpecific()
863  {
864  loadedSprites.ForEach(s => s.Remove());
865  loadedSprites.Clear();
866  ResetWidgets();
867  // Automatically reload all sprites that have been selected at least once (and thus might have been edited)
868  var reloadedSprites = new List<Sprite>();
869  foreach (var sprite in dirtySprites)
870  {
871  foreach (var s in Sprite.LoadedSprites)
872  {
873  if (s.FullPath == sprite.FullPath && !reloadedSprites.Contains(s))
874  {
875  s.ReloadXML();
876  reloadedSprites.Add(s);
877  }
878  }
879  }
880  dirtySprites.Clear();
881  filterSpritesBox.Text = "";
882  filterTexturesBox.Text = "";
883  }
884 
885  public void SelectSprite(Sprite sprite)
886  {
887  lastSprite = sprite;
888  if (!loadedSprites.Contains(sprite))
889  {
890  loadedSprites.Add(sprite);
891  RefreshLists();
892  }
893  if (selectedSprites.Any(s => s.FullPath != selectedTexturePath))
894  {
895  ResetWidgets();
896  }
897  if (Widget.EnableMultiSelect)
898  {
899  if (selectedSprites.Contains(sprite))
900  {
901  selectedSprites.Remove(sprite);
902  }
903  else
904  {
905  selectedSprites.Add(sprite);
906  dirtySprites.Add(sprite);
907  }
908  }
909  else
910  {
911  selectedSprites.Clear();
912  selectedSprites.Add(sprite);
913  dirtySprites.Add(sprite);
914  }
915  if (sprite.FullPath != selectedTexturePath)
916  {
917  textureList.Select(sprite.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled);
918  UpdateScrollBar(textureList);
919  }
920  xmlPathText.Text = string.Empty;
921  foreach (var s in selectedSprites)
922  {
923  texturePathText.Text = s.FilePath.Value;
924  var element = s.SourceElement;
925  if (element != null)
926  {
927  string xmlPath = element.ParseContentPathFromUri();
928  if (!xmlPathText.Text.Contains(xmlPath))
929  {
930  xmlPathText.Text += "\n" + xmlPath;
931  }
932  }
933  }
934  xmlPathText.TextColor = Color.LightGray;
935  }
936 
937  public void RefreshLists()
938  {
939  selectedSprites.Clear();
940  textureList.ClearChildren();
941  spriteList.ClearChildren();
942  ResetWidgets();
943  HashSet<string> textures = new HashSet<string>();
944  // Create texture list
945  foreach (Sprite sprite in loadedSprites.OrderBy(s => Path.GetFileNameWithoutExtension(s.FilePath.Value)))
946  {
947  //ignore sprites that don't have a file path (e.g. submarine pics)
948  if (sprite.FilePath.IsNullOrEmpty()) { continue; }
949  string normalizedFilePath = sprite.FilePath.FullPath;
950  if (!textures.Contains(normalizedFilePath))
951  {
952  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textureList.Content.RectTransform) { MinSize = new Point(0, 20) },
953  Path.GetFileName(sprite.FilePath.Value))
954  {
955  ToolTip = sprite.FilePath.Value,
956  UserData = sprite.FullPath
957  };
958  textures.Add(normalizedFilePath);
959  }
960  }
961  // Create sprite list
962  // TODO: allow the user to choose whether to sort by file name or by texture sheet
963  //foreach (Sprite sprite in loadedSprites.OrderBy(s => GetSpriteName(s)))
964  foreach (Sprite sprite in loadedSprites.OrderBy(s => s.SourceElement.GetAttributeContentPath("texture")?.Value ?? string.Empty))
965  {
966  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), spriteList.Content.RectTransform) { MinSize = new Point(0, 20) },
967  GetSpriteName(sprite) + " (" + sprite.SourceRect.X + ", " + sprite.SourceRect.Y + ", " + sprite.SourceRect.Width + ", " + sprite.SourceRect.Height + ")")
968  {
969  UserData = sprite
970  };
971  }
972  topPanelContents.Visible = false;
973  }
974 
975  public void ResetZoom()
976  {
977  if (SelectedTexture == null) { return; }
978  var viewArea = GetViewArea;
979  float width = viewArea.Width / (float)SelectedTexture.Width;
980  float height = viewArea.Height / (float)SelectedTexture.Height;
981  zoom = Math.Min(1, Math.Min(width, height));
982  zoomBar.BarScroll = GetBarScrollValue();
983  viewAreaOffset = Point.Zero;
984  }
985 #endregion
986 
987  #region Helpers
988  private Point viewAreaOffset;
989  private Rectangle GetViewArea
990  {
991  get
992  {
993  int margin = 20;
994  var viewArea = new Rectangle(leftPanel.Rect.Right + margin + viewAreaOffset.X, topPanel.Rect.Bottom + margin + viewAreaOffset.Y, rightPanel.Rect.Left - leftPanel.Rect.Right - margin * 2, Frame.Rect.Height - topPanel.Rect.Height - margin * 2);
995  return viewArea;
996  }
997  }
998 
999  private float GetBarScrollValue() => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(MinZoom, MaxZoom, zoom));
1000 
1001  private string GetSpriteName(Sprite sprite)
1002  {
1003  var sourceElement = sprite.SourceElement;
1004  if (sourceElement == null) { return string.Empty; }
1005  string name = sprite.Name;
1006  if (string.IsNullOrWhiteSpace(name))
1007  {
1008  name = sourceElement.Parent.GetAttributeString("identifier", string.Empty);
1009  }
1010  if (string.IsNullOrEmpty(name))
1011  {
1012  name = sourceElement.Parent.GetAttributeString("name", string.Empty);
1013  }
1014  return string.IsNullOrEmpty(name) ? Path.GetFileNameWithoutExtension(sprite.FilePath.Value) : name;
1015  }
1016 
1017  private void UpdateScrollBar(GUIListBox listBox)
1018  {
1019  var sb = listBox.ScrollBar;
1020  sb.BarScroll = MathHelper.Clamp(MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, listBox.Content.CountChildren - 1, listBox.SelectedIndex)), sb.MinValue, sb.MaxValue);
1021  }
1022 
1023  private void UpdateSourceRect(Sprite sprite, Rectangle newRect)
1024  {
1025  sprite.SourceRect = newRect;
1026  // Keeps the relative origin unchanged. The absolute origin will be recalculated.
1027  sprite.RelativeOrigin = sprite.RelativeOrigin;
1028  }
1029 #endregion
1030 
1031  #region Widgets
1032  private Dictionary<string, Widget> widgets = new Dictionary<string, Widget>();
1033 
1034  private Widget GetWidget(string id, Sprite sprite, int size = 5, WidgetShape shape = WidgetShape.Rectangle, Action<Widget> initMethod = null)
1035  {
1036  if (!widgets.TryGetValue(id, out Widget widget))
1037  {
1038  int selectedSize = (int)Math.Round(size * 1.5f);
1039  widget = new Widget(id, size, shape)
1040  {
1041  Data = sprite,
1042  Color = Color.Yellow,
1043  SecondaryColor = Color.Gray,
1044  TooltipOffset = new Vector2(selectedSize / 2 + 5, -10)
1045  };
1046  widget.PreDraw += (sp, dTime) =>
1047  {
1048  if (!widget.IsControlled)
1049  {
1050  widget.Refresh();
1051  }
1052  };
1053  widget.PreUpdate += dTime => widget.Enabled = selectedSprites.Contains(sprite);
1054  widget.PostUpdate += dTime =>
1055  {
1056  widget.InputAreaMargin = widget.IsControlled ? 1000 : 0;
1057  widget.Size = widget.IsSelected ? selectedSize : size;
1058  widget.IsFilled = widget.IsControlled;
1059  };
1060  widgets.Add(id, widget);
1061  initMethod?.Invoke(widget);
1062  }
1063  return widget;
1064  }
1065 
1066  private void ResetWidgets()
1067  {
1068  widgets.Clear();
1069  Widget.SelectedWidgets.Clear();
1070  }
1071 #endregion
1072  }
1073 }
string???????????? Value
Definition: ContentPath.cs:27
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
virtual Rectangle Rect
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
void Select(object userData, Force force=Force.No, AutoScroll autoScroll=AutoScroll.Enabled)
Definition: GUIListBox.cs:440
override void ClearChildren()
Definition: GUIListBox.cs:1243
OnTextChangedHandler OnTextChanged
Don't set the Text property on delegates that register to this event, because modifying the Text will...
Definition: GUITextBox.cs:38
static RasterizerState ScissorTestEnable
Definition: GameMain.cs:195
Action ResolutionChanged
NOTE: Use very carefully. You need to ensure that you ALWAYS unsubscribe from this when you no longer...
Definition: GameMain.cs:133
static GameMain Instance
Definition: GameMain.cs:144
static bool KeyDown(InputType inputType)
Vector2 RelativeSize
Relative to the parent rect.
Point?? MinSize
Min size in pixels. Does not affect scaling.
bool Contains(string str, StringComparison stringComparison=StringComparison.Ordinal)
override void Update(double deltaTime)
override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
override void AddToGUIUpdateList()
By default, submits the screen's main GUIFrame and, if requested upon construction,...
static IEnumerable< Sprite > LoadedSprites
NumberType
Definition: Enums.cs:715
WidgetShape
Definition: Widget.cs:11