#region ================== Namespaces using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Linq; using System.Windows.Forms; #endregion namespace CodeImp.DoomBuilder.Controls { //mxd. Based on (but heavily reworked since) TextureListPanel from Sledge (https://github.com/LogicAndTrick/sledge) internal class ImageSelectorPanel : Panel { #region ================== Constants #endregion #region ================== Variables private VScrollBar scrollbar; private List items; private List selection; private List rectangles; private ImageBrowserItem lastselecteditem; private int imagesize = 128; private bool classicview = false; private bool usedtexturesfirst = false; private string contenttype = "Textures"; private string title; private int titleheight { get { return classicview ? 0 : SystemFonts.MessageBoxFont.Height + 6; } } //mxd. Tooltips private ToolTip tooltip; private Point lasttooltippos; private const int tooltipreshowdistance = 48; //mxd. Textures cache private static Dictionary> texturecache = new Dictionary>(); // > private static Dictionary> flatcache = new Dictionary>(); // > // Selection private bool allowselection; private bool allowmultipleselection; private bool centeritem; private List itemgroups = new List(); #endregion #region ================== Event handlers public delegate void ItemSelectedEventHandler(object sender, ImageBrowserItem item); public delegate void SelectionChangedEventHandler(object sender, List selection); /*public event ItemSelectedEventHandler ItemSelected; private void OnItemSelected(ImageBrowserItem item) { if(ItemSelected != null) ItemSelected(this, item); }*/ public event SelectionChangedEventHandler SelectionChanged; private void OnSelectionChanged(List selection) { if(SelectionChanged != null) SelectionChanged(this, selection); } public event ItemSelectedEventHandler ItemDoubleClicked; private void OnItemDoubleClicked(ImageBrowserItem item) { if(ItemDoubleClicked != null) ItemDoubleClicked(this, item); } #endregion #region ================== Properties public bool HideSelection { get { return !allowselection; } set { allowselection = !value; if(!allowselection && selection.Count > 0) { selection.Clear(); Refresh(); } } } public bool MultiSelect { get { return allowmultipleselection; } set { allowmultipleselection = value; if(!allowmultipleselection && selection.Count > 0) { var first = selection[0]; selection.Clear(); selection.Add(first); Refresh(); } } } public int ImageSize { get { return imagesize; } set { imagesize = value; UpdateRectangles(); if(selection.Count > 0) ScrollToItem(selection[0]); } } public bool ClassicView { get { return classicview; } set { classicview = value; UpdateRectangles(); if (selection.Count > 0) ScrollToItem(selection[0]); } } public bool UsedTexturesFirst { get { return usedtexturesfirst; } set { usedtexturesfirst = value; UpdateRectangles(); if (selection.Count > 0) ScrollToItem(selection[0]); } } public bool CenterItem { get { return centeritem; } set { centeritem = value; } } public List Items { get { return items; } } public List SelectedItems { get { return selection; } } public string Title { get { return title; } set { title = value; } } public string ContentType { get { return contenttype; } set { contenttype = value; } } // why do encapsulation when it's not needed? #endregion #region ================== Constructor / Disposer public ImageSelectorPanel() { VScroll = true; AutoScroll = true; DoubleBuffered = true; scrollbar = new VScrollBar { Dock = DockStyle.Right }; scrollbar.ValueChanged += (sender, e) => Refresh(); tooltip = new ToolTip(); //mxd items = new List(); selection = new List(); imagesize = 128; rectangles = new List(); title = "All images:"; Controls.Add(scrollbar); } protected override void Dispose(bool disposing) { if(disposing) Clear(); base.Dispose(disposing); } #endregion #region ================== Add/Remove/Get Textures //mxd. Clears the list without redrawing it public void Clear() { selection.Clear(); items.Clear(); lastselecteditem = null; rectangles.Clear(); } //mxd public void ClearSelection() { selection.Clear(); lastselecteditem = null; OnSelectionChanged(selection); Refresh(); } public void SetItems(IEnumerable itemgroups) { this.items.Clear(); lastselecteditem = null; selection.Clear(); this.itemgroups.Clear(); foreach (ImageBrowserItemGroup ibig in itemgroups) { this.items.AddRange(ibig.Items); this.itemgroups.Add(ibig); } OnSelectionChanged(selection); UpdateRectangles(); } public void SetItems(IEnumerable items) { this.items.Clear(); lastselecteditem = null; selection.Clear(); this.items.AddRange(items); OnSelectionChanged(selection); UpdateRectangles(); } public void SetSelectedItem(ImageBrowserItem item) { SetSelectedItems(new List { item }); } public void SetSelectedItems(List items) { selection.Clear(); if(items.Count > 0) { selection.AddRange(items); ScrollToItem(items[0]); //mxd Refresh(); //mxd } OnSelectionChanged(selection); } public void ScrollToItem(ImageBrowserItem item) { ScrollToItem(item, centeritem); } public void ScrollToItem(ImageBrowserItem item, bool centeritem) { int index = items.IndexOf(item); if(index < 0) return; Rectangle rec = rectangles[index]; //mxd. Already visible? int ymin = scrollbar.Value - titleheight; int ymax = ymin + this.ClientRectangle.Height + titleheight; if(rec.Top - 3 >= ymin && rec.Bottom + 3 <= ymax) return; int yscroll; if (centeritem) yscroll = rec.Top + rec.Height / 2 - ClientRectangle.Height / 2; else yscroll = rec.Top - titleheight - 3; scrollbar.Value = Math.Max(0, Math.Min(yscroll, scrollbar.Maximum - ClientRectangle.Height)); ; Refresh(); } /// /// Jumps back and forth between the currently selected texture at the top and in the pile of all textures /// /// The item to jump back and forth between public void JumpBetweenItems(ImageBrowserItem item) { // The item has to exist twice, once in the used textures at the top and in the pile, so get the indexes int index1 = items.IndexOf(item); int index2 = items.LastIndexOf(item); // Item only exists once, so stop here if (index1 == index2) return; Rectangle rect1 = rectangles[index1]; Rectangle rect2 = rectangles[index2]; // Get the distance to the two images. Count from the center of the currently scrolled to position and take the bottom // of the first image and the top of the second image int distance1 = Math.Abs(rect1.Bottom - (scrollbar.Value + ClientRectangle.Height / 2)); int distance2 = Math.Abs(rect2.Top - (scrollbar.Value + ClientRectangle.Height / 2)); int yscroll; // Get the y position we want to scroll to if(centeritem) yscroll = (distance1 > distance2 ? (rect1.Top + rect1.Height / 2) : (rect2.Top + rect2.Height / 2)) - ClientRectangle.Height / 2; else yscroll = distance1 > distance2 ? rect1.Top : rect2.Top; // The desired y position might be outside of the range we can scroll to, so clamp the value scrollbar.Value = Math.Max(0, Math.Min(yscroll, scrollbar.Maximum - ClientRectangle.Height)); Refresh(); } public void SelectNextItem(SearchDirectionHint dir) { if(!allowselection) return; if(selection.Count == 0) { if(items.Count > 0) SetSelectedItem(items[0]); return; } int targetindex = items.IndexOf(selection[0]); Rectangle rect = rectangles[targetindex]; int index, newindex, tx, cx, cy; switch(dir) { case SearchDirectionHint.Right: // Just select the next item if(targetindex < items.Count - 1) SetSelectedItem(items[targetindex + 1]); break; case SearchDirectionHint.Left: // Just select the previous item if(targetindex > 0) SetSelectedItem(items[targetindex - 1]); break; case SearchDirectionHint.Up: // Skip current row... index = targetindex - 1; if(index < 0) break; while(index > 0) { if(rectangles[index].Y != rect.Y) break; index--; } // Check upper row for best match tx = rect.X + rect.Width / 2; cx = int.MaxValue; cy = rectangles[index].Y; newindex = int.MaxValue; while(index >= 0 && rectangles[index].Y == cy) { int ccx = Math.Abs(rectangles[index].X + rectangles[index].Width / 2 - tx); if(ccx < cx) { cx = ccx; newindex = index; } index--; } // Select item if(newindex != int.MaxValue) SetSelectedItem(items[newindex]); break; case SearchDirectionHint.Down: // Skip current row... index = targetindex + 1; if(index > rectangles.Count - 1) break; while(index < rectangles.Count - 1) { if(rectangles[index].Y != rect.Y) break; index++; } // Check upper row for best match tx = rect.X + rect.Width / 2; cx = int.MaxValue; cy = rectangles[index].Y; newindex = int.MaxValue; while(index < rectangles.Count && rectangles[index].Y == cy) { int ccx = Math.Abs(rectangles[index].X + rectangles[index].Width / 2 - tx); if(ccx < cx) { cx = ccx; newindex = index; } index++; } // Select item if(newindex != int.MaxValue) SetSelectedItem(items[newindex]); break; } } protected override void OnMouseDoubleClick(MouseEventArgs e) { base.OnMouseDoubleClick(e); if(General.Interface.CtrlState || General.Interface.ShiftState || selection.Count != 1) return; int index = GetIndexAt(e.X, scrollbar.Value + e.Y); if(index == -1) return; OnItemDoubleClicked(items[index]); } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); this.Focus(); if(!allowselection) return; if(!allowmultipleselection || !General.Interface.CtrlState) selection.Clear(); int x = e.X; int y = scrollbar.Value + e.Y; int clickedIndex = GetIndexAt(x, y); var item = (clickedIndex >= 0 && clickedIndex < items.Count ? items[clickedIndex] : null); if(item == null) { selection.Clear(); } else if(allowmultipleselection && General.Interface.CtrlState && selection.Contains(item)) { selection.Remove(item); lastselecteditem = null; } else if(allowmultipleselection && General.Interface.ShiftState && lastselecteditem != null) { int bef = items.IndexOf(lastselecteditem); var start = Math.Min(bef, clickedIndex); var count = Math.Abs(clickedIndex - bef) + 1; selection.AddRange(items.GetRange(start, count).Where(i => !selection.Contains(i))); } else { selection.Add(item); lastselecteditem = item; } OnSelectionChanged(selection); Refresh(); } //mxd protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); int index = GetIndexAt(e.X, scrollbar.Value + e.Y); if(index == -1 || items[index].ItemType != ImageBrowserItemType.IMAGE || string.IsNullOrEmpty(items[index].ToolTip)) { if(tooltip.Active) tooltip.Hide(this); } else if(!tooltip.Active || tooltip.GetToolTip(this) != items[index].ToolTip || Math.Abs(lasttooltippos.X - e.Location.X) > tooltipreshowdistance || Math.Abs(lasttooltippos.Y - e.Location.Y) > tooltipreshowdistance) { Point pos = new Point(e.Location.X, e.Location.Y + Cursor.Size.Height + 4); tooltip.Show(items[index].ToolTip, this, pos, 999999); lasttooltippos = e.Location; } } public int GetIndexAt(int x, int y) { int padhorz = classicview?13:2; int padvert = 2; int font = 4 + SystemFonts.MessageBoxFont.Height; for(var i = 0; i < rectangles.Count; i++) { var rec = rectangles[i]; if(rec.Left - padhorz <= x && rec.Right + padhorz >= x && rec.Top - padvert <= y && rec.Bottom + padvert + font >= y) { return i; } } return -1; } #endregion #region ================== Scrolling private void ScrollByAmount(int value) { int newvalue = Math.Max(0, scrollbar.Value + value); scrollbar.Value = Math.Min(newvalue, Math.Max(0, scrollbar.Maximum - ClientRectangle.Height)); } protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); ScrollByAmount(scrollbar.SmallChange * (e.Delta / -120)); } //mxd. Otherwise arrow keys won't be handled by OnKeyDown protected override bool IsInputKey(Keys keyData) { switch(keyData) { case Keys.Right: case Keys.Left: case Keys.Up: case Keys.Down: case Keys.Return: return true; } return base.IsInputKey(keyData); } protected override void OnKeyDown(KeyEventArgs e) { switch(e.KeyCode) { //mxd. Cursor keys case Keys.Left: SelectNextItem(SearchDirectionHint.Left); break; case Keys.Right: SelectNextItem(SearchDirectionHint.Right); break; case Keys.Up: SelectNextItem(SearchDirectionHint.Up); break; case Keys.Down: SelectNextItem(SearchDirectionHint.Down); break; case Keys.PageDown: ScrollByAmount(scrollbar.LargeChange); break; case Keys.PageUp: ScrollByAmount(-scrollbar.LargeChange); break; case Keys.End: ScrollByAmount(int.MaxValue); break; case Keys.Home: ScrollByAmount(-int.MaxValue); break; case Keys.Enter: if(selection.Count > 0) OnItemDoubleClicked(selection[0]); break; } base.OnKeyDown(e); } #endregion #region ================== Updating Rectangles & Dimensions protected override void OnResize(EventArgs e) { base.OnResize(e); UpdateRectangles(); //mxd if(selection.Count > 0) ScrollToItem(selection[0]); } internal void UpdateRectangles() { int w = ClientRectangle.Width - scrollbar.Width; int padhorz = classicview?13:2; int padvert = 2; int font = 4 + SystemFonts.MessageBoxFont.Height; int cx = 0; int cy = titleheight; int my = 0; rectangles.Clear(); var firstItem = (items.Count > 0) ? items[0] : null; foreach (ImageBrowserItemGroup ibig in itemgroups) { if (classicview) { // new row, also provide space for category name. cx = 0; cy += SystemFonts.MessageBoxFont.Height + 6 + my + 16 /* ((ti != firstItem) ? 16 : 0) */; my = 0; } foreach (var ti in ibig.Items) { Image preview = GetPreview(ti, imagesize); int rw = w - cx; int wid = Math.Max((imagesize > 0 ? imagesize : preview.Width), ti.TextureNameWidth) + padhorz + padhorz; int hei = (imagesize > 0 ? imagesize : preview.Height) + padvert + padvert + font; // if we draw height below, add another font if (General.Settings.ShowTextureSizes && General.Settings.TextureSizesBelow && ti.ItemType == ImageBrowserItemType.IMAGE) hei += font; if (rw < wid) { // New row cx = 0; cy += my; my = 0; } my = Math.Max(my, hei); var rect = new Rectangle(cx + padhorz, cy + padvert, wid - padhorz - padhorz, hei - padvert - padvert - font); rectangles.Add(rect); cx += wid; } } if(rectangles.Count > 0) { scrollbar.Maximum = cy + my; scrollbar.SmallChange = (imagesize > 0 ? imagesize : 128) + padvert + padvert + font; scrollbar.LargeChange = ClientRectangle.Height; scrollbar.Visible = (scrollbar.Maximum > ClientRectangle.Height); if(scrollbar.Value > scrollbar.Maximum - ClientRectangle.Height) { scrollbar.Value = Math.Max(0, scrollbar.Maximum - ClientRectangle.Height); } } else { scrollbar.Visible = false; } Refresh(); } #endregion #region ================== Rendering protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); DrawTextures(e.Graphics); } private void DrawTextureHeader(Graphics g, string text, Rectangle rec, bool background) { bool blackbrowsers = (General.Settings != null && General.Settings.BlackBrowsers); if (background) { // Draw group name bg Color bgcolor = (blackbrowsers ? Color.Gray : SystemColors.Control); using (Brush bg = new SolidBrush(Color.FromArgb(192, bgcolor))) { g.FillRectangle(bg, rec.X, rec.Y, rec.Width, rec.Height); } } else { // Draw underline Color underlinecolor = blackbrowsers ? Color.FromArgb(0x7FFFFFFF) : Color.FromArgb((SystemColors.ControlText.ToArgb()&0xFFFFFF)|0x7F000000); g.DrawLine(new Pen(underlinecolor), rec.Left, rec.Bottom, rec.Right - 2, rec.Bottom); } // Draw group name Brush fgbrush = (blackbrowsers ? Brushes.White : SystemBrushes.ControlText); Font bf = new Font(SystemFonts.MessageBoxFont, FontStyle.Bold); g.DrawString(text, bf, fgbrush, rec.X, rec.Y); } private void DrawTextures(Graphics g) { // Draw items int scrollwidth = (scrollbar.Visible ? scrollbar.Width : 0); if (items.Count > 0) { int y = scrollbar.Value; int height = ClientRectangle.Height; int i = 0; ImageBrowserItemType currentType = ImageBrowserItemType.IMAGE; // initialized to random value bool currentUsedInMap = false; ImageBrowserItem.SetBrushes(classicview, rectangles[0].X, rectangles[0].Y - y, rectangles[0].Width, rectangles[0].Height); foreach (ImageBrowserItemGroup ibig in itemgroups) { if (classicview) { // draw corresponding title right above this item. string hdrname = ibig.Title + ":"; DrawTextureHeader(g, hdrname, new Rectangle(2, rectangles[i].Y - (SystemFonts.MessageBoxFont.Height + 6) - y, ClientRectangle.Width - scrollwidth - 4, SystemFonts.MessageBoxFont.Height), false); currentType = items[i].ItemType; currentUsedInMap = items[i].Icon.UsedInMap; } for(int j=0; j < ibig.Items.Count; j++, i++) { Rectangle rec = rectangles[i]; if (rec.Bottom < y) continue; if (rec.Top > y + height) break; Image bmp = GetPreview(items[i], imagesize); items[i].Draw(g, bmp, rec.X, rec.Y - y, rec.Width, rec.Height, selection.Contains(items[i]), items[i].Icon.UsedInMap, classicview); } } } // Draw title on top of items if(!string.IsNullOrEmpty(title) && !classicview) { DrawTextureHeader(g, title, new Rectangle(2, 2, ClientRectangle.Width - scrollwidth - 4, SystemFonts.MessageBoxFont.Height), true); } } #endregion #region ================== Image Caching // [ZZ] used during resource reload. public static void ClearCachedPreviews() { foreach (Dictionary imgdict in texturecache.Values) foreach (Image img in imgdict.Values) img.Dispose(); foreach (Dictionary imgdict in flatcache.Values) foreach (Image img in imgdict.Values) img.Dispose(); texturecache.Clear(); flatcache.Clear(); } private static Image GetPreview(ImageBrowserItem item, int imagesize) { if(!item.IsPreviewLoaded) return item.Icon.GetPreview(); Dictionary> cache = item.Icon.IsFlat ? flatcache : texturecache; if(!cache.ContainsKey(imagesize)) cache.Add(imagesize, new Dictionary()); // Generate preview? // [ZZ] how does this work when texture images change? if(!cache[imagesize].ContainsKey(item.Icon.LongName)) { Image img = item.Icon.GetPreview(); // Determine preview size float scalex, scaley; if(item.ItemType == ImageBrowserItemType.IMAGE) { scalex = (imagesize == 0 ? 1.0f : (imagesize / (float)img.Width)); scaley = (imagesize == 0 ? 1.0f :(imagesize / (float)img.Height)); } else { // Don't upscale folder icons scalex = (imagesize == 0 ? 1.0f : ((img.Width > imagesize) ? (imagesize / (float)img.Width) : 1.0f)); scaley = (imagesize == 0 ? 1.0f : ((img.Height > imagesize) ? (imagesize / (float)img.Height) : 1.0f)); } float scale = Math.Min(scalex, scaley); int previewwidth = (int)(img.Width * scale); int previewheight = (int)(img.Height * scale); if(previewwidth < 1) previewwidth = 1; if(previewheight < 1) previewheight = 1; // Make new image Bitmap preview = new Bitmap(previewwidth, previewheight, PixelFormat.Format32bppArgb); using(Graphics g = Graphics.FromImage(preview)) { g.PageUnit = GraphicsUnit.Pixel; g.InterpolationMode = InterpolationMode.NearestNeighbor; g.PixelOffsetMode = PixelOffsetMode.Half; g.DrawImage(img, new Rectangle(0, 0, previewwidth, previewheight)); } cache[imagesize][item.Icon.LongName] = preview; } // Get preview return cache[imagesize][item.Icon.LongName]; } #endregion } }