#region ================== Copyright (c) 2007 Pascal vd Heiden /* * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com * This program is released under GNU General Public License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #endregion #region ================== Namespaces using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Windows.Forms; using CodeImp.DoomBuilder.Data; using CodeImp.DoomBuilder.Windows; #endregion namespace CodeImp.DoomBuilder.Controls { internal partial class ImageBrowserControl : UserControl { #region ================== Constants #endregion #region ================== Delegates / Events public delegate void SelectedItemChangedDelegate(); public delegate void SelectedItemDoubleClickDelegate(); public event SelectedItemChangedDelegate SelectedItemChanged; public event SelectedItemDoubleClickDelegate SelectedItemDoubleClicked; #endregion #region ================== Variables // Properties private bool preventselection; // States private bool updating; private int keepselected; private bool browseFlats; //mxd private static bool uselongtexturenames; //mxd private static bool showtexturesfromsubdirs; //mxd private int currentlevel; //mxd // All items private readonly List items; // Items visible in the list private List visibleitems; //mxd private static int mixMode; #endregion #region ================== Properties public bool PreventSelection { get { return preventselection; } set { preventselection = value; } } public bool HideInputBox { get { return splitter.Panel2Collapsed; } set { splitter.Panel2Collapsed = value; } } public bool BrowseFlats { get { return browseFlats; } set { browseFlats = value; } } //mxd public static bool ShowTexturesFromSubDirectories { get { return showtexturesfromsubdirs; } internal set { showtexturesfromsubdirs = value; } } //mxd public static bool UseLongTextureNames { get { return uselongtexturenames; } internal set { uselongtexturenames = value; } } //mxd public ListViewItem SelectedItem { get { if(list.SelectedItems.Count > 0) return list.SelectedItems[0]; else return null; } } #endregion #region ================== Constructor / Disposer // Constructor public ImageBrowserControl() { // Initialize InitializeComponent(); items = new List(); //mxd StepsList sizes = new StepsList { 4, 8, 16, 32, 48, 64, 96, 128, 196, 256, 512, 1024 }; filterWidth.StepValues = sizes; filterHeight.StepValues = sizes; //mxd. Looks like SplitterDistance is unaffected by DPI scaling. Let's fix that... if(MainForm.DPIScaler.Height != 1.0f) { splitter.SplitterDistance = splitter.Height - splitter.Panel2.Height - (int)Math.Round(splitter.SplitterWidth * MainForm.DPIScaler.Height); } } // This applies the application settings public void ApplySettings() { // Force black background? if(General.Settings.BlackBrowsers) { list.BackColor = Color.Black; list.ForeColor = Color.White; } // Set the size of preview images if(General.Map != null) { int itemwidth = General.Map.Data.Previews.MaxImageWidth + 26; int itemheight = General.Map.Data.Previews.MaxImageHeight + 26; list.TileSize = new Size(itemwidth, itemheight); //mxd if(General.Map.Config.MixTexturesFlats) { cbMixMode.SelectedIndex = mixMode; } else { labelMixMode.Visible = false; cbMixMode.Visible = false; int offset = label.Left - labelMixMode.Left; label.Left -= offset; objectname.Left -= offset; filterWidth.Left -= offset; filterwidthlabel.Left -= offset; filterHeight.Left -= offset; filterheightlabel.Left -= offset; showsubdirtextures.Left -= offset; longtexturenames.Left -= offset; mixMode = 0; } //mxd. Use long texture names? longtexturenames.Checked = (uselongtexturenames && General.Map.Config.UseLongTextureNames); longtexturenames.Visible = General.Map.Config.UseLongTextureNames; if(!General.Map.Config.UseLongTextureNames) showsubdirtextures.Left = longtexturenames.Left; //mxd } else { longtexturenames.Visible = false; //mxd uselongtexturenames = false; //mxd showsubdirtextures.Left = longtexturenames.Left; //mxd } //mxd if(!General.Settings.CapitalizeTextureNames) objectname.CharacterCasing = CharacterCasing.Normal; //mxd. Show textures in subfolders? showsubdirtextures.Checked = showtexturesfromsubdirs; } // This cleans everything up public virtual void CleanUp() { // Stop refresh timer refreshtimer.Enabled = false; } #endregion #region ================== Rendering // Draw item private void list_DrawItem(object sender, DrawListViewItemEventArgs e) { if(!updating) (e.Item as ImageBrowserItem).Draw(e.Graphics, e.Bounds); } // Refresher private void refreshtimer_Tick(object sender, EventArgs e) { bool allpreviewsloaded = true; // Go for all items foreach(ImageBrowserItem i in list.Items) { // Check if there are still previews that are not loaded allpreviewsloaded &= i.IsPreviewLoaded; // Items needs to be redrawn? if(i.CheckRedrawNeeded()) { // Refresh item in list //list.RedrawItems(i.Index, i.Index, false); list.Invalidate(); } } // If all previews were loaded, stop this timer if(allpreviewsloaded) refreshtimer.Stop(); } #endregion #region ================== Events // Name typed private void objectname_TextChanged(object sender, EventArgs e) { // Update list RefillList(false); // No item selected? if(list.SelectedItems.Count == 0) { // Select first SelectFirstItem(); } } // Key pressed in textbox private void objectname_KeyDown(object sender, KeyEventArgs e) { // Check what key is pressed switch(e.KeyData) { // Cursor keys case Keys.Left: SelectNextItem(SearchDirectionHint.Left); e.SuppressKeyPress = true; break; case Keys.Right: SelectNextItem(SearchDirectionHint.Right); e.SuppressKeyPress = true; break; case Keys.Up: SelectNextItem(SearchDirectionHint.Up); e.SuppressKeyPress = true; break; case Keys.Down: SelectNextItem(SearchDirectionHint.Down); e.SuppressKeyPress = true; break; // Tab case Keys.Tab: GoToNextSameTexture(); e.SuppressKeyPress = true; break; } } //mxd. Handle keyboard navigation the same way regardless of list being focused... private void list_KeyDown(object sender, KeyEventArgs e) { // Check what key is pressed switch(e.KeyData) { // Cursor keys case Keys.Left: SelectNextItem(SearchDirectionHint.Left); e.SuppressKeyPress = true; break; case Keys.Right: SelectNextItem(SearchDirectionHint.Right); e.SuppressKeyPress = true; break; case Keys.Up: SelectNextItem(SearchDirectionHint.Up); e.SuppressKeyPress = true; break; case Keys.Down: SelectNextItem(SearchDirectionHint.Down); e.SuppressKeyPress = true; break; } } //mxd private void filterSize_WhenTextChanged(object sender, EventArgs e) { objectname_TextChanged(sender, e); } //mxd protected override bool ProcessTabKey(bool forward) { GoToNextSameTexture(); return false; } // Selection changed private void list_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e) { if(!e.IsSelected) return; //mxd. Don't want to trigger this twice // Prevent selecting? if(preventselection) { foreach(ListViewItem i in list.SelectedItems) i.Selected = false; } else { // Raise event if(SelectedItemChanged != null) SelectedItemChanged(); } } // Doublelicking an item private void list_DoubleClick(object sender, EventArgs e) { if(!preventselection && (list.SelectedItems.Count > 0)) if(SelectedItemDoubleClicked != null) SelectedItemDoubleClicked(); } //mxd. Transfer focus to Filter textbox private void list_KeyPress(object sender, KeyPressEventArgs e) { if(!General.Settings.KeepTextureFilterFocused) return; objectname.Focus(); if(e.KeyChar == '\b') // Any better way to check for Backspace?.. { if(!string.IsNullOrEmpty(objectname.Text) && objectname.SelectionStart > 0 && objectname.SelectionLength == 0) { int s = objectname.SelectionStart - 1; objectname.Text = objectname.Text.Remove(s, 1); objectname.SelectionStart = s; } } else { objectname.AppendText(e.KeyChar.ToString(CultureInfo.InvariantCulture)); } } //mxd private void cbMixMode_SelectedIndexChanged(object sender, EventArgs e) { mixMode = cbMixMode.SelectedIndex; RefillList(false); } //mxd private void longtexturenames_CheckedChanged(object sender, EventArgs e) { uselongtexturenames = longtexturenames.Checked; RefillList(false); } //mxd private void showsubdirtextures_CheckedChanged(object sender, EventArgs e) { showtexturesfromsubdirs = showsubdirtextures.Checked; RefillList(false); } #endregion #region ================== Methods // This selects the next texture with the same name as the selected texture public void GoToNextSameTexture() { if(list.SelectedItems.Count > 0) { list.Focus(); //mxd ListViewItem selected = list.SelectedItems[0]; //mxd foreach(ImageBrowserItem n in visibleitems) { if(n == selected) continue; if(n.Text == selected.Text) { if(list.IsGroupCollapsed(n.Group)) list.SetGroupCollapsed(n.Group, false); n.Selected = true; n.Focused = true; n.EnsureVisible(); return; } } } } // This selects an item by longname (mxd - changed from name to longname) public void SelectItem(long longname, ListViewGroup preferredgroup) { ImageBrowserItem lvi = null; //mxd // Not when selecting is prevented if(preventselection) return; // Search in preferred group first if(preferredgroup != null) { foreach(ListViewItem item in list.Items) { ImageBrowserItem curitem = item as ImageBrowserItem; if(curitem != null && longname == curitem.Icon.LongName) //mxd { lvi = curitem; if(item.Group == preferredgroup) break; } } } // Select the item if(lvi != null) { // Select this item list.SelectedItems.Clear(); lvi.Selected = true; lvi.EnsureVisible(); } } // This performs item sleection by keys private void SelectNextItem(SearchDirectionHint dir) { // Not when selecting is prevented if(preventselection) return; // Nothing selected? if(list.SelectedItems.Count == 0) { // Select first SelectFirstItem(); } else { //mxd int index = list.SelectedItems[0].Index; int targetindex = -1; ListViewGroup startgroup = list.SelectedItems[0].Group; Rectangle startrect = list.SelectedItems[0].GetBounds(ItemBoundsPortion.Entire); switch(dir) { // Check previous items untill groups match... case SearchDirectionHint.Left: if(list.SelectedIndices[0] > 0) { while(--index > -1) { if(list.Items[index].Group == startgroup) { targetindex = index; break; } } } break; // Same thing, other direction... case SearchDirectionHint.Right: if(list.SelectedIndices[0] < list.Items.Count - 1) { while(++index < list.Items.Count) { if(list.Items[index].Group == startgroup) { targetindex = index; break; } } } break; // Check previous items untill X coordinate match and Y coordinate is less than the start ones... case SearchDirectionHint.Up: while(--index > -1) { ListViewItem item = list.Items[index]; if(item != null && item.Group == startgroup) { Rectangle rect = item.GetBounds(ItemBoundsPortion.Entire); if(rect.X == startrect.X && rect.Y < startrect.Y) { targetindex = index; break; } } } break; // Same thing, other direction... case SearchDirectionHint.Down: if(list.SelectedIndices[0] < list.Items.Count - 1) { while(++index < list.Items.Count) { ListViewItem item = list.Items[index]; if(item != null && item.Group == startgroup) { Rectangle rect = item.GetBounds(ItemBoundsPortion.Entire); if(rect.X == startrect.X && rect.Y > startrect.Y) { targetindex = index; break; } } } } break; } //mxd. Use the old method for Up/Down keys, becaue it can jump between Groups... if(targetindex == -1 && (dir == SearchDirectionHint.Up || dir == SearchDirectionHint.Down)) { Point spos = new Point(startrect.Location.X + startrect.Width / 2, startrect.Y + startrect.Height / 2); // Try finding 5 times in the given direction for(int i = 0; i < 5; i++) { // Move point in given direction switch(dir) { case SearchDirectionHint.Up: spos.Y -= list.TileSize.Height / 2; break; case SearchDirectionHint.Down: spos.Y += list.TileSize.Height / 2; break; } // Test position ListViewItem lvi = list.GetItemAt(spos.X, spos.Y); if(lvi != null) { targetindex = lvi.Index; break; } } } //mxd. Found something?.. if(targetindex != -1) { // Select item list.SelectedItems.Clear(); list.Items[targetindex].Selected = true; list.SelectedItems[0].EnsureVisible(); } } } // This selectes the first item private void SelectFirstItem() { // Not when selecting is prevented if(preventselection) return; // Select first if(list.Items.Count > 0) { list.SelectedItems.Clear(); ListViewItem lvi = list.GetItemAt(list.TileSize.Width / 2, list.TileSize.Height / 2); if(lvi != null) { lvi.Selected = true; lvi.EnsureVisible(); } } } // This adds a group public ListViewGroup AddGroup(string name) { ListViewGroup grp = new ListViewGroup(name); list.Groups.Add(grp); return grp; } //mxd public bool IsGroupCollapsed(ListViewGroup group) { if(!list.Groups.Contains(group)) return false; return list.IsGroupCollapsed(group); } //mxd. This enables group collapsability and optionally collapses it public void SetGroupCollapsed(ListViewGroup group, bool collapse) { if(!list.Groups.Contains(group)) return; list.SetGroupCollapsed(group, collapse); } // This begins adding items public void BeginAdding(bool keepselectedindex) { BeginAdding(0, keepselectedindex); } //mxd public void BeginAdding(int selectedlevel, bool keepselectedindex) { if(keepselectedindex && (list.SelectedItems.Count > 0)) keepselected = list.SelectedIndices[0]; else keepselected = -1; currentlevel = selectedlevel; // Clean list items.Clear(); // Stop updating refreshtimer.Enabled = false; } // This ends adding items public void EndAdding() { // Fill list with items RefillList(true); // Start updating refreshtimer.Enabled = true; } // This adds an item public void Add(ImageData image, object tag, ListViewGroup group) { ImageBrowserItem i = new ImageBrowserItem(image, tag, uselongtexturenames); //mxd i.ListGroup = group; i.Group = group; i.ToolTipText = image.Name; //mxd items.Add(i); } // This adds an item public void Add(ImageData image, object tag, ListViewGroup group, string tooltiptext) { ImageBrowserItem i = new ImageBrowserItem(image, tag, uselongtexturenames); //mxd i.ListGroup = group; i.Group = group; i.ToolTipText = tooltiptext; items.Add(i); } // This fills the list based on the objectname filter private void RefillList(bool selectfirst) { visibleitems = new List(); //mxd. Store info about currently selected item string selectedname = string.Empty; ListViewGroup selecteditemgroup = null; if(!selectfirst && keepselected == -1 && list.SelectedIndices.Count > 0) { selectedname = list.Items[list.SelectedIndices[0]].Text; selecteditemgroup = list.Items[list.SelectedIndices[0]].Group; } // Begin updating list updating = true; //list.SuspendLayout(); list.BeginUpdate(); // Clear list first // Group property of items will be set to null, we will restore it later list.Items.Clear(); //mxd. Filtering by texture size? int w = filterWidth.GetResult(-1); int h = filterHeight.GetResult(-1); // Go for all items ImageBrowserItem previtem = null; //mxd for(int i = items.Count - 1; i > -1; i--) { // Add item if valid items[i].ShowFullName = uselongtexturenames; //mxd if(ValidateItem(items[i], previtem) && ValidateItemSize(items[i], w, h)) { items[i].Group = items[i].ListGroup; items[i].Selected = false; visibleitems.Add(items[i]); previtem = items[i]; } } // Fill list visibleitems.Sort(); ListViewItem[] array = new ListViewItem[visibleitems.Count]; for(int i = 0; i < visibleitems.Count; i++) array[i] = visibleitems[i]; list.Items.AddRange(array); // Done updating list updating = false; list.EndUpdate(); list.Invalidate(); //list.ResumeLayout(); // Make selection? if(!preventselection && (list.Items.Count > 0)) { // Select specific item? if(keepselected > -1) { list.Items[keepselected].Selected = true; list.Items[keepselected].EnsureVisible(); } // Select first item? else if(selectfirst) { SelectFirstItem(); } //mxd. Try reselecting the same/next closest item else if(selecteditemgroup != null && !string.IsNullOrEmpty(selectedname)) { ListViewItem bestmatch = null; int charsmatched = 1; foreach(ListViewItem item in list.Items) { if(item.Group == selecteditemgroup && item.Text[0] == selectedname[0]) { if(item.Text == selectedname) { bestmatch = item; break; } for(int i = 1; i < Math.Min(item.Text.Length, selectedname.Length); i++) { if(item.Text[i] != selectedname[i]) { if(i > charsmatched) { bestmatch = item; charsmatched = i; } break; } } } } // Select the first item from the same group... if(bestmatch == null) { foreach(ListViewItem item in list.Items) { if(item.Group == selecteditemgroup) { bestmatch = item; break; } } } // Select found item if(bestmatch != null) { bestmatch.Selected = true; bestmatch.EnsureVisible(); } } } // Raise event if((SelectedItemChanged != null) && !preventselection) SelectedItemChanged(); } // This validates an item private bool ValidateItem(ImageBrowserItem item, ImageBrowserItem previtem) { //mxd. Don't show duplicate items if(previtem != null && item.TextureName == previtem.TextureName && item.Group == previtem.Group) return false; //mxd //mxd. mixMode: 0 = All, 1 = Textures, 2 = Flats, 3 = Based on BrowseFlats if(!splitter.Panel2Collapsed) { if(mixMode == 1 && item.Icon.IsFlat) return false; if(mixMode == 2 && !item.Icon.IsFlat) return false; if(mixMode == 3 && (browseFlats != item.Icon.IsFlat)) return false; if(!showtexturesfromsubdirs && item.Icon.Level > currentlevel) return false; } return item.Text.ToUpperInvariant().Contains(objectname.Text.ToUpperInvariant()); } //mxd. This validates an item's texture size private static bool ValidateItemSize(ImageBrowserItem i, int w, int h) { if(!i.Icon.IsPreviewLoaded) return true; if(w > 0 && i.Icon.Width != w) return false; if(h > 0 && i.Icon.Height != h) return false; return true; } // This sends the focus to the textbox public void FocusTextbox() { if(General.Settings.KeepTextureFilterFocused) //mxd objectname.Focus(); else list.Focus(); } #endregion } }