diff --git a/BeefLibs/Beefy2D/src/theme/dark/DarkIconButton.bf b/BeefLibs/Beefy2D/src/theme/dark/DarkIconButton.bf index 401b5894..2aec13d9 100644 --- a/BeefLibs/Beefy2D/src/theme/dark/DarkIconButton.bf +++ b/BeefLibs/Beefy2D/src/theme/dark/DarkIconButton.bf @@ -3,19 +3,107 @@ using System.Collections; using System.Text; using Beefy.widgets; using Beefy.gfx; +using Beefy.geom; namespace Beefy.theme.dark { + public struct Padding + { + public float Left; + public float Right; + public float Top; + public float Bottom; + + public this(float padding) + { + Left = padding; + Right = padding; + Top = padding; + Bottom = padding; + } + + public this(float leftRight, float topBottom) + { + Left = leftRight; + Right = leftRight; + Top = topBottom; + Bottom = topBottom; + } + + public this(float left, float right, float top, float bottom) + { + Left = left; + Right = right; + Top = top; + Bottom = bottom; + } + } + public class DarkIconButton : ButtonWidget { - public Image mIcon; - public float mIconOfsX; - public float mIconOfsY; + private Image mIcon; + + private Padding mPadding = .(4); + + public Image Icon + { + get => mIcon; + set + { + if (mIcon == value) + return; + + mIcon = value; + + if (mIcon != null) + UpdateSize(); + } + } + + public Padding Padding + { + get => mPadding; + set + { + if (mPadding == value) + return; + + mPadding = value; + + UpdateSize(); + } + } + + /// Calculates the size of the button. + private void UpdateSize() + { + float width = mPadding.Left + mIcon.mWidth + mPadding.Right; + float height = mPadding.Top + mIcon.mHeight + mPadding.Bottom; + + Resize(0, 0, width, height); + } public override void Draw(Graphics g) { base.Draw(g); - g.Draw(mIcon, mIconOfsX, mIconOfsY); + + bool drawDown = ((mMouseDown && mMouseOver) || (mMouseFlags.HasFlag(MouseFlag.Kbd))); + + Image texture = + mDisabled ? DarkTheme.sDarkTheme.GetImage(.BtnUp) : + drawDown ? DarkTheme.sDarkTheme.GetImage(.BtnDown) : + mMouseOver ? DarkTheme.sDarkTheme.GetImage(.BtnOver) : + DarkTheme.sDarkTheme.GetImage(.BtnUp); + + g.DrawBox(texture, 0, 0, mWidth, mHeight); + + if ((mHasFocus) && (!mDisabled)) + { + using (g.PushColor(DarkTheme.COLOR_SELECTED_OUTLINE)) + g.DrawBox(DarkTheme.sDarkTheme.GetImage(.Outline), 0, 0, mWidth, mHeight); + } + + g.Draw(mIcon, mPadding.Left, mPadding.Top); } } } diff --git a/BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf b/BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf index 2ff33e08..ea915594 100644 --- a/BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf +++ b/BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf @@ -184,6 +184,13 @@ namespace Beefy.theme.dark CollapseClosed, CollapseOpened, + IconBookmarkDisabled, + NewBookmarkFolder, + PrevBookmark, + NextBookmark, + PrevBookmarkInFolder, + NextBookmarkInFolder, + COUNT }; diff --git a/IDE/dist/images/DarkUI.png b/IDE/dist/images/DarkUI.png index 151d4f4b..524a2579 100644 Binary files a/IDE/dist/images/DarkUI.png and b/IDE/dist/images/DarkUI.png differ diff --git a/IDE/dist/images/DarkUI.psd b/IDE/dist/images/DarkUI.psd index fc54eba2..abdf27ed 100644 Binary files a/IDE/dist/images/DarkUI.psd and b/IDE/dist/images/DarkUI.psd differ diff --git a/IDE/dist/images/DarkUI_2.png b/IDE/dist/images/DarkUI_2.png index 99dff161..4e31fc6d 100644 Binary files a/IDE/dist/images/DarkUI_2.png and b/IDE/dist/images/DarkUI_2.png differ diff --git a/IDE/dist/images/DarkUI_4.png b/IDE/dist/images/DarkUI_4.png index 0e970d43..740858a5 100644 Binary files a/IDE/dist/images/DarkUI_4.png and b/IDE/dist/images/DarkUI_4.png differ diff --git a/IDE/dist/images/DarkUI_4.psd b/IDE/dist/images/DarkUI_4.psd index 7f642106..0e48c48b 100644 Binary files a/IDE/dist/images/DarkUI_4.psd and b/IDE/dist/images/DarkUI_4.psd differ diff --git a/IDE/src/BookmarkManager.bf b/IDE/src/BookmarkManager.bf index c921fde1..6cadde35 100644 --- a/IDE/src/BookmarkManager.bf +++ b/IDE/src/BookmarkManager.bf @@ -48,79 +48,474 @@ namespace IDE } public class Bookmark : TrackedTextElement - { + { + public String mTitle ~ delete _; public String mNotes ~ delete _; + public bool mIsDisabled; + public BookmarkFolder mFolder; + + internal int IndexInFolder => mFolder.mBookmarkList.IndexOf(this); } + public class BookmarkFolder + { + /// The title of the bookmark-folder that will be visible in the bookmark-panel + public String mTitle ~ delete _; + + public List mBookmarkList = new List() ~ delete _; + + /// Gets or Sets whether every bookmark in this folder is disabled or not. + public bool IsDisabled + { + get + { + for (var bookmark in mBookmarkList) + if (!bookmark.mIsDisabled) + return false; + + return true; + } + set + { + for (var bookmark in mBookmarkList) + bookmark.mIsDisabled = value; + + gApp.mBookmarkManager.BookmarksChanged(); + } + } + + /// Adds the given bookmark to this folder. If needed, removes it from its old folder. + internal void AddBookmark(Bookmark bookmark) + { + if (bookmark.mFolder != null) + { + bookmark.mFolder.mBookmarkList.Remove(bookmark); + } + mBookmarkList.Add(bookmark); + bookmark.mFolder = this; + } + + internal int Index => gApp.mBookmarkManager.mBookmarkFolders.IndexOf(this); + } + + using internal Bookmark; + using internal BookmarkFolder; + public class BookmarkManager { - public List mBookmarkList = new List() ~ - { - for (var bookmark in mBookmarkList) - bookmark.Kill(); - delete _; - }; - public int32 mBookmarkIdx; + public BookmarkFolder mRootFolder = new .(); + public List mBookmarkFolders = new .() {mRootFolder} ~ + { + while (!_.IsEmpty) + { + DeleteFolder(_.Back); + } - public Bookmark CreateBookmark(String fileName, int wantLineNum, int wantColumn) + delete _; + }; + + /// Occurs when a bookmark/folder is added, removed or moved. + public Event BookmarksChanged ~ _.Dispose(); + + public delegate void MovedToBookmarkEventHandler(Bookmark bookmark); + + // Occurs when the user jumped to a bookmark. + public Event MovedToBookmark ~ _.Dispose(); + + private int mBookmarkCount; + + /// Number of bookmarks created, used to generate the names. + private int32 _createdBookmarks; + /// Number of folders created, used to generate the names. + private int32 _createdFolders; + + /// Gets or sets whether all bookmarks are disabled or not. + public bool AllBookmarksDisabled + { + get + { + for (var folder in mBookmarkFolders) + { + if (!folder.IsDisabled) + return false; + } + + return true; + } + set + { + for (var folder in mBookmarkFolders) + { + folder.IsDisabled = value; + } + + BookmarksChanged(); + } + } + + /// Gets the currently selected bookmark or null, if no bookmark is selected. + public Bookmark CurrentBookmark + { + get; + private set; + } + + public BookmarkFolder CurrentFolder => CurrentBookmark?.mFolder; + + /** + * Creates a new bookmark folder + * @param title The title of the bookmark + * @returns the newly created BookmarkFolder + */ + public BookmarkFolder CreateFolder(String title = null) + { + BookmarkFolder folder = new .(); + + if (title == null) + folder.mTitle = new $"Folder {_createdFolders++}"; + else + folder.mTitle = new String(title); + + mBookmarkFolders.Add(folder); + + BookmarksChanged(); + + return folder; + } + + /// Deletes the given bookmark folder and all bookmarks inside it. + public void DeleteFolder(BookmarkFolder folder) + { + if (folder == CurrentFolder) + { + // the current bookmark will be deleted, so we have to find another one + int folderIdx = mBookmarkFolders.IndexOf(folder); + int newFolderIdx = folderIdx; + + BookmarkFolder newFolder = null; + + repeat + { + newFolderIdx--; + + if (newFolderIdx < 0) + { + newFolderIdx = mBookmarkFolders.Count - 1; + } + + if (mBookmarkFolders[newFolderIdx].mBookmarkList.Count != 0) + { + newFolder = mBookmarkFolders[newFolderIdx]; + break; + } + } + // Break when we reach start + while ((folderIdx != newFolderIdx)); + + if (newFolder != null) + { + Debug.Assert(newFolder.mBookmarkList.Count != 0); + + CurrentBookmark = newFolder.mBookmarkList[0]; + } + } + + while (!folder.mBookmarkList.IsEmpty) + { + gApp.mBookmarkManager.DeleteBookmark(folder.mBookmarkList.Back); + } + + mBookmarkFolders.Remove(folder); + + delete folder; + + if (gApp.mDebugger.mBreakpointsChangedDelegate.HasListeners) + gApp.mDebugger.mBreakpointsChangedDelegate(); + + BookmarksChanged(); + } + + public Bookmark CreateBookmark(String fileName, int wantLineNum, int wantColumn, bool isDisabled = false, String title = null, BookmarkFolder folder = null) { - mBookmarkIdx = (int32)mBookmarkList.Count; + var folder; + + folder = folder ?? mRootFolder; Bookmark bookmark = new Bookmark(); bookmark.mFileName = new String(fileName); bookmark.mLineNum = (int32)wantLineNum; bookmark.mColumn = (int32)wantColumn; - mBookmarkList.Add(bookmark); - gApp.mDebugger.mBreakpointsChangedDelegate(); + if (title == null) + bookmark.mTitle = new $"Bookmark {_createdBookmarks++}"; + else + bookmark.mTitle = new String(title); + + bookmark.mIsDisabled = isDisabled; + + folder.[Friend]AddBookmark(bookmark); + + if (gApp.mDebugger.mBreakpointsChangedDelegate.HasListeners) + gApp.mDebugger.mBreakpointsChangedDelegate(); + + CurrentBookmark = bookmark; + BookmarksChanged(); + + mBookmarkCount++; + return bookmark; } public void DeleteBookmark(Bookmark bookmark) { - int idx = mBookmarkList.IndexOf(bookmark); - mBookmarkList.RemoveAt(idx); - if (mBookmarkIdx == idx) - mBookmarkIdx--; - if (mBookmarkIdx >= mBookmarkList.Count) - mBookmarkIdx = (int32)mBookmarkList.Count - 1; - gApp.mDebugger.mBreakpointsChangedDelegate(); + bool deletedCurrentBookmark = false; + Bookmark newCurrentBookmark = null; + + if (bookmark == CurrentBookmark) + { + deletedCurrentBookmark = true; + + // try to select a bookmark from the current folder + newCurrentBookmark = FindPrevBookmark(true); + + if (newCurrentBookmark == null || newCurrentBookmark == CurrentBookmark) + { + // Current folder is empty, select from previous folder + newCurrentBookmark = FindPrevBookmark(false); + + if (newCurrentBookmark == CurrentBookmark) + { + // We have to make sure, that we don't select the current bookmark + newCurrentBookmark = null; + } + } + } + + BookmarkFolder folder = bookmark.mFolder; + + folder.mBookmarkList.Remove(bookmark); + bookmark.Kill(); + + if (deletedCurrentBookmark) + CurrentBookmark = newCurrentBookmark; + + if (gApp.mDebugger.mBreakpointsChangedDelegate.HasListeners) + gApp.mDebugger.mBreakpointsChangedDelegate(); + + BookmarksChanged(); + + mBookmarkCount--; } - public void Clear() + enum Placement { - for (var bookmark in mBookmarkList) - bookmark.Kill(); - mBookmarkList.Clear(); - mBookmarkIdx = 0; - gApp.mDebugger.mBreakpointsChangedDelegate(); + Before, + After } - public void PrevBookmark() + /** Moves the bookmark to the specified folder. + * @param bookmark The bookmark to move. + * @param folder The folder to which the bookmark will be moved. + * @param insertBefore If null the bookmark will be added at the end of the folder. Otherwise it will be inserted before the specified bookmark. + */ + public void MoveBookmarkToFolder(Bookmark bookmark, BookmarkFolder targetFolder, Placement place = .Before, Bookmark targetBookmark = null) + { + if (bookmark.mFolder != null) + { + bookmark.mFolder.mBookmarkList.Remove(bookmark); + } + + if (targetBookmark == null) + targetFolder.mBookmarkList.Add(bookmark); + else + { + Debug.Assert(targetFolder == targetBookmark.mFolder, "Insert before must be in folder."); + + int index = targetFolder.mBookmarkList.IndexOf(targetBookmark); + + if (place == .After) + index++; + + targetFolder.mBookmarkList.Insert(index, bookmark); + } + + bookmark.mFolder = targetFolder; + + BookmarksChanged(); + } + + /** Moves the given folder in front of or behind the given target-folder. + * @param folder The folder to move. + * @param place Specifies whether folder will be placed in front of or behind target. + * @param target The folder relative to which folder will be placed. + */ + public void MoveFolder(BookmarkFolder folder, Placement place = .After, BookmarkFolder target = null) + { + if (folder == target) + return; + + if (mBookmarkFolders.Contains(folder)) + mBookmarkFolders.Remove(folder); + + if (target == null) + { + mBookmarkFolders.Add(folder); + } + else + { + int index = mBookmarkFolders.IndexOf(target); + + if (place == .After) + index++; + + mBookmarkFolders.Insert(index, folder); + } + + BookmarksChanged(); + } + + /// Deletes all bookmarks and bookmark-folders. + public void Clear() + { + for (var folder in mBookmarkFolders) + { + for (var bookmark in folder.mBookmarkList) + bookmark.Kill(); + folder.mBookmarkList.Clear(); + } + ClearAndDeleteItems!(mBookmarkFolders); + + mRootFolder = new BookmarkFolder(); + mBookmarkFolders.Add(mRootFolder); + + if (gApp.mDebugger.mBreakpointsChangedDelegate.HasListeners) + gApp.mDebugger.mBreakpointsChangedDelegate(); + + mBookmarkCount = 0; + + BookmarksChanged(); + } + + /** Finds and returns the previous bookmark relative to CurrentBookmark. + * @param currentFolderOnly If set to true, only the current folder will be searched. Otherwise all folders will be searched. + * @returns The previous bookmark. If CurrentBookmark is the only bookmark, CurrentBookmark will be returned. Null, if there are no bookmarks. + */ + private Bookmark FindPrevBookmark(bool currentFolderOnly) + { + if (mBookmarkCount == 0) + return null; + + int currentFolderIdx = CurrentFolder.Index; + int currentBookmarkIdx = CurrentBookmark.IndexInFolder; + + Bookmark prevBookmark = null; + + int newFolderIndex = currentFolderIdx; + int newBmIndex = currentBookmarkIdx; + + repeat + { + newBmIndex--; + + if (newBmIndex < 0) + { + if (!currentFolderOnly) + { + newFolderIndex--; + + if (newFolderIndex < 0) + { + // wrap to last folder + newFolderIndex = (int32)mBookmarkFolders.Count - 1; + } + } + + // Select last bookmark in current folder + newBmIndex = (int32)mBookmarkFolders[newFolderIndex].mBookmarkList.Count - 1; + } + + if (newBmIndex >= 0) + prevBookmark = mBookmarkFolders[newFolderIndex].mBookmarkList[newBmIndex]; + else + prevBookmark = null; + } + // skip disabled bookmarks, stop when we reach starting point + while ((prevBookmark == null || prevBookmark.mIsDisabled) && ((currentFolderIdx != newFolderIndex) || (currentBookmarkIdx != newBmIndex) && newBmIndex != -1)); + + return prevBookmark; + } + + /** Jumps to the previous bookmark. + * @param currentFolderOnly If true, only the current folder will be searched. Otherwise all folders will be searched. + */ + public void PrevBookmark(bool currentFolderOnly = false) { - if (mBookmarkList.Count == 0) - return; + Bookmark prevBookmark = FindPrevBookmark(currentFolderOnly); - mBookmarkIdx--; - if (mBookmarkIdx < 0) - mBookmarkIdx = (int32)mBookmarkList.Count - 1; + // If prevBookmark is disabled no bookmark is enabled. + if (prevBookmark != null && !prevBookmark.mIsDisabled) + GotoBookmark(prevBookmark); + } + + /* Jumps to the next bookmark. + * @param currentFolderOnly If true, only the current folder will be searched. Otherwise all folders will be searched. + */ + public void NextBookmark(bool currentFolderOnly = false) + { + if (mBookmarkCount == 0) + return; - var bookmark = mBookmarkList[mBookmarkIdx]; - gApp.ShowSourceFileLocation(bookmark.mFileName, -1, -1, bookmark.mLineNum, bookmark.mColumn, LocatorType.Smart); + int currentFolderIdx = CurrentFolder.Index; + int currentBookmarkIdx = CurrentBookmark.IndexInFolder; + + Bookmark nextBookmark = null; + + int newFolderIndex = currentFolderIdx; + int newBmIndex = currentBookmarkIdx; + + repeat + { + newBmIndex++; + + if (newBmIndex >= mBookmarkFolders[newFolderIndex].mBookmarkList.Count) + { + if (!currentFolderOnly) + { + newFolderIndex++; + + if (newFolderIndex >= mBookmarkFolders.Count) + { + // wrap to first folder + newFolderIndex = 0; + } + } + + // Select first bookmark in current folder (or -1 if there is no bookmark) + newBmIndex = mBookmarkFolders[newFolderIndex].mBookmarkList.IsEmpty ? -1 : 0; + } + + if (newBmIndex >= 0) + nextBookmark = mBookmarkFolders[newFolderIndex].mBookmarkList[newBmIndex]; + else + nextBookmark = null; + } + // skip disabled bookmarks, stop when we reach starting point + while ((nextBookmark == null || nextBookmark.mIsDisabled) && ((currentFolderIdx != newFolderIndex) || (currentBookmarkIdx != newBmIndex) && newBmIndex != -1)); + + // If nextBookmark is disabled no bookmark is enabled. + if (nextBookmark != null && !nextBookmark.mIsDisabled) + GotoBookmark(nextBookmark); } - public void NextBookmark() - { - if (mBookmarkList.Count == 0) - return; + /// Moves the cursor to the given bookmark. + public void GotoBookmark(Bookmark bookmark) + { + CurrentBookmark = bookmark; - mBookmarkIdx++; - if (mBookmarkIdx >= mBookmarkList.Count) - mBookmarkIdx = 0; + gApp.ShowSourceFileLocation(bookmark.mFileName, -1, -1, bookmark.mLineNum, bookmark.mColumn, LocatorType.Smart); - var bookmark = mBookmarkList[mBookmarkIdx]; - gApp.ShowSourceFileLocation(bookmark.mFileName, -1, -1, bookmark.mLineNum, bookmark.mColumn, LocatorType.Smart); - } + MovedToBookmark(bookmark); + } } } diff --git a/IDE/src/Commands.bf b/IDE/src/Commands.bf index 8476f587..f4c5ca86 100644 --- a/IDE/src/Commands.bf +++ b/IDE/src/Commands.bf @@ -197,6 +197,8 @@ namespace IDE Add("Autocomplete", new => gApp.Cmd_ShowAutoComplete, .None); Add("Bookmark Next", new => gApp.Cmd_NextBookmark, .Editor); Add("Bookmark Prev", new => gApp.Cmd_PrevBookmark, .Editor); + Add("Bookmark Next in Folder", new => gApp.Cmd_NextBookmarkInFolder, .Editor); + Add("Bookmark Prev in Folder", new => gApp.Cmd_PrevBookmarkInFolder, .Editor); Add("Bookmark Toggle", new => gApp.Cmd_ToggleBookmark, .Editor); Add("Bookmark Clear", new => gApp.Cmd_ClearBookmarks, .Editor); Add("Break All", new => gApp.[Friend]Cmd_Break); @@ -295,6 +297,7 @@ namespace IDE Add("Settings", new => gApp.ShowSettings); Add("Show Auto Watches", new => gApp.ShowAutoWatches); Add("Show Autocomplete Panel", new => gApp.ShowAutoCompletePanel); + Add("Show Bookmarks", new => gApp.ShowBookmarks); Add("Show Breakpoints", new => gApp.ShowBreakpoints); Add("Show Call Stack", new => gApp.ShowCallstack); Add("Show Class View", new => gApp.ShowClassViewPanel); diff --git a/IDE/src/IDEApp.bf b/IDE/src/IDEApp.bf index 22d90f59..7e70fd23 100644 --- a/IDE/src/IDEApp.bf +++ b/IDE/src/IDEApp.bf @@ -213,6 +213,7 @@ namespace IDE public Widget mLastActivePanel; public SourceViewPanel mLastActiveSourceViewPanel; public AutoCompletePanel mAutoCompletePanel; + public BookmarksPanel mBookmarksPanel; public Rect mRequestedWindowRect = Rect(64, 64, 1200, 1024); public BFWindow.ShowKind mRequestedShowKind; @@ -703,6 +704,7 @@ namespace IDE RemoveAndDelete!(mProfilePanel); RemoveAndDelete!(mPropertiesPanel); RemoveAndDelete!(mAutoCompletePanel); + RemoveAndDelete!(mBookmarksPanel); if (mSymbolReferenceHelper != null) mSymbolReferenceHelper.Close(); @@ -807,6 +809,7 @@ namespace IDE dlg(mProfilePanel); dlg(mPropertiesPanel); dlg(mAutoCompletePanel); + dlg(mBookmarksPanel); } public override void ShutdownCompleted() @@ -1916,23 +1919,37 @@ namespace IDE } } } - - using (sd.CreateArray("Bookmarks")) + + using (sd.CreateArray("BookmarkFolders")) { - for (var bookmark in mBookmarkManager.mBookmarkList) - { - if (bookmark.mFileName != null) + for (var folder in mBookmarkManager.mBookmarkFolders) + { + using (sd.CreateObject()) { - using (sd.CreateObject()) - { - String relPath = scope .(); - mWorkspace.GetWorkspaceRelativePath(bookmark.mFileName, relPath); - sd.Add("File", relPath); - sd.Add("Line", bookmark.mLineNum); - sd.Add("Column", bookmark.mColumn); - } - } - } + sd.Add("Title", folder.mTitle); + + using (sd.CreateArray("Bookmarks")) + { + for (var bookmark in folder.mBookmarkList) + { + if (bookmark.mFileName != null) + { + using (sd.CreateObject()) + { + String relPath = scope .(); + mWorkspace.GetWorkspaceRelativePath(bookmark.mFileName, relPath); + sd.Add("File", relPath); + sd.Add("Line", bookmark.mLineNum); + sd.Add("Column", bookmark.mColumn); + sd.Add("Title", bookmark.mTitle); + if (bookmark.mIsDisabled) + sd.Add("Disabled", true); + } + } + } + } + } + } } using (sd.CreateObject("DebuggerDisplayTypes")) @@ -2426,6 +2443,10 @@ namespace IDE mErrorsPanel.Clear(); + mBookmarksPanel.Clear(); + + mBookmarkManager.Clear(); + OutputLine("Workspace closed."); } @@ -3333,17 +3354,33 @@ namespace IDE breakpoint.SetThreadId(0); } } - - for (var _bookmark in data.Enumerate("Bookmarks")) + + for (var _bookmarkFolder in data.Enumerate("BookmarkFolders")) { - String relPath = scope String(); - data.GetString("File", relPath); - IDEUtils.FixFilePath(relPath); - String absPath = scope String(); - mWorkspace.GetWorkspaceAbsPath(relPath, absPath); - int32 lineNum = data.GetInt("Line"); - int32 column = data.GetInt("Column"); - mBookmarkManager.CreateBookmark(absPath, lineNum, column); + String title = scope String(); + data.GetString("Title", title); + + BookmarkFolder folder = null; + + if (!String.IsNullOrWhiteSpace(title)) + folder = mBookmarkManager.CreateFolder(title); + + for (var _bookmark in data.Enumerate("Bookmarks")) + { + String relPath = scope String(); + data.GetString("File", relPath); + IDEUtils.FixFilePath(relPath); + String absPath = scope String(); + mWorkspace.GetWorkspaceAbsPath(relPath, absPath); + int32 lineNum = data.GetInt("Line"); + int32 column = data.GetInt("Column"); + String bookmarkTitle = scope String(); + data.GetString("Title", bookmarkTitle); + + bool isDisabled = data.GetBool("Disabled", false); + + mBookmarkManager.CreateBookmark(absPath, lineNum, column, isDisabled, bookmarkTitle, folder); + } } for (var referenceId in data.Enumerate("DebuggerDisplayTypes")) @@ -3911,6 +3948,10 @@ namespace IDE { watchPanel.TryRenameItem(); } + else if (var bookmarksPanel = activePanel as BookmarksPanel) + { + bookmarksPanel.TryRenameItem(); + } } [IDECommand] @@ -3968,13 +4009,25 @@ namespace IDE [IDECommand] public void Cmd_PrevBookmark() { - mBookmarkManager.PrevBookmark(); + mBookmarkManager.PrevBookmark(false); + } + + [IDECommand] + public void Cmd_PrevBookmarkInFolder() + { + mBookmarkManager.PrevBookmark(true); } [IDECommand] public void Cmd_NextBookmark() { - mBookmarkManager.NextBookmark(); + mBookmarkManager.NextBookmark(false); + } + + [IDECommand] + public void Cmd_NextBookmarkInFolder() + { + mBookmarkManager.NextBookmark(true); } [IDECommand] @@ -5006,6 +5059,12 @@ namespace IDE ShowPanel(mProfilePanel, "Profile"); } + [IDECommand] + public void ShowBookmarks() + { + ShowPanel(mBookmarksPanel, "Bookmarks"); + } + [IDECommand] public void ShowQuickWatch() { @@ -5712,6 +5771,7 @@ namespace IDE subMenu = root.AddMenuItem("&View"); AddMenuItem(subMenu, "AutoComplet&e", "Show Autocomplete Panel"); AddMenuItem(subMenu, "&Auto Watches", "Show Auto Watches"); + AddMenuItem(subMenu, "Boo&kmarks", "Show Bookmarks"); AddMenuItem(subMenu, "&Breakpoints", "Show Breakpoints"); AddMenuItem(subMenu, "&Call Stack", "Show Call Stack"); AddMenuItem(subMenu, "C&lass View", "Show Class View"); @@ -12019,6 +12079,8 @@ namespace IDE mPropertiesPanel.mAutoDelete = false; mAutoCompletePanel = new AutoCompletePanel(); mAutoCompletePanel.mAutoDelete = false; + mBookmarksPanel = new BookmarksPanel(); + mBookmarksPanel.mAutoDelete = false; GetVersionInfo(var exeDate); let localExeDate = exeDate.ToLocalTime(); diff --git a/IDE/src/ui/BookmarksPanel.bf b/IDE/src/ui/BookmarksPanel.bf new file mode 100644 index 00000000..2118643b --- /dev/null +++ b/IDE/src/ui/BookmarksPanel.bf @@ -0,0 +1,565 @@ +using Beefy.theme.dark; +using Beefy.utils; +using Beefy.widgets; +using System; +using System.Collections; +using Beefy.theme; +using Beefy.events; +using System.Diagnostics; +using Beefy.theme.dark; +using Beefy.gfx; +using Beefy.geom; + +namespace IDE.ui +{ + public class BookmarksListView : IDEListView + { + protected override ListViewItem CreateListViewItem() + { + return new BookmarksListViewItem(); + } + protected override void SetScaleData() + { + base.SetScaleData(); + mIconX = GS!(200); + mOpenButtonX = GS!(0); + mLabelX = GS!(0); + //mChildIndent = GS!(16); + mHiliteOffset = GS!(-2); + } + } + + public class BookmarksListViewItem : IDEListViewItem + { + public Object RefObject; + public String BookmarkLine ~ delete _; + + public float mLabelOffsetFolder = GS!(16); + public float mLabelOffsetBookmark = GS!(0); + + public override void RehupScale(float oldScale, float newScale) + { + base.RehupScale(oldScale, newScale); + + mLabelOffsetFolder = GS!(16); + mLabelOffsetBookmark = GS!(0); + } + + protected override float GetLabelOffset() + { + if (RefObject is BookmarkFolder) + { + return mLabelOffsetFolder; + } + + return mLabelOffsetBookmark; + } + + public void Goto() + { + if (Bookmark bookmark = RefObject as Bookmark) + { + gApp.mBookmarkManager.GotoBookmark(bookmark); + } + } + } + + class BookmarksPanel : Panel + { + public DarkIconButton mBtnCreateBookmarkFolder; + public DarkIconButton mBtnPrevBookmark; + public DarkIconButton mBtnNextBookmark; + public DarkIconButton mBtnPrevBookmarkInFolder; + public DarkIconButton mBtnNextBookmarkInFolder; + + public BookmarksListView mBookmarksListView; + + public this() + { + mBtnCreateBookmarkFolder = new DarkIconButton(); + mBtnCreateBookmarkFolder.Icon = DarkTheme.sDarkTheme.GetImage(.NewBookmarkFolder); + mBtnCreateBookmarkFolder.mOnMouseClick.Add(new (args) => gApp.mBookmarkManager.CreateFolder()); + AddWidget(mBtnCreateBookmarkFolder); + + float iconButtonWidth = mBtnCreateBookmarkFolder.Width; + + mBtnPrevBookmark = new DarkIconButton(); + mBtnPrevBookmark.Icon = DarkTheme.sDarkTheme.GetImage(.PrevBookmark); + mBtnPrevBookmark.mOnMouseClick.Add(new (args) => gApp.Cmd_PrevBookmark()); + mBtnPrevBookmark.X = GS!(1) + iconButtonWidth; + AddWidget(mBtnPrevBookmark); + + mBtnNextBookmark = new DarkIconButton(); + mBtnNextBookmark.Icon = DarkTheme.sDarkTheme.GetImage(.NextBookmark); + mBtnNextBookmark.mOnMouseClick.Add(new (args) => gApp.Cmd_NextBookmark()); + mBtnNextBookmark.X = (GS!(1) + iconButtonWidth) * 2; + AddWidget(mBtnNextBookmark); + + mBtnPrevBookmarkInFolder = new DarkIconButton(); + mBtnPrevBookmarkInFolder.Icon = DarkTheme.sDarkTheme.GetImage(.PrevBookmarkInFolder); + mBtnPrevBookmarkInFolder.mOnMouseClick.Add(new (args) => gApp.Cmd_PrevBookmarkInFolder()); + mBtnPrevBookmarkInFolder.X = (GS!(1) + iconButtonWidth) * 3; + AddWidget(mBtnPrevBookmarkInFolder); + + mBtnNextBookmarkInFolder = new DarkIconButton(); + mBtnNextBookmarkInFolder.Icon = DarkTheme.sDarkTheme.GetImage(.NextBookmarkInFolder); + mBtnNextBookmarkInFolder.mOnMouseClick.Add(new (args) => gApp.Cmd_NextBookmarkInFolder()); + mBtnNextBookmarkInFolder.X = (GS!(1) + iconButtonWidth) * 4; + AddWidget(mBtnNextBookmarkInFolder); + + + mBookmarksListView = new .(); + mBookmarksListView.mOnEditDone.Add(new => HandleEditDone); + + mBookmarksListView.InitScrollbars(true, true); + mBookmarksListView.mLabelX = GS!(6); + mBookmarksListView.mOnItemMouseClicked.Add(new => ListViewItemMouseClicked); + + mBookmarksListView.AddColumn(200, "Bookmark"); + mBookmarksListView.AddColumn(400, "File"); + mBookmarksListView.AddColumn(120, "Line"); + + mBookmarksListView.mOnDragEnd.Add(new => BookmarksLV_OnDragEnd); + mBookmarksListView.mOnDragUpdate.Add(new => BookmarksLV_OnDragUpdate); + + mBookmarksListView.mOnItemMouseDown.Add(new (item, x, y, btnNum, btnCount) => + { + if ((btnNum == 0) && (btnCount == 2)) + { + let mainItem = (BookmarksListViewItem)item.GetSubItem(0); + mainItem.Goto(); + } + + ListViewItemMouseDown(item, x, y, btnNum, btnCount); + }); + mBookmarksListView.mOnItemMouseClicked.Add(new => ListViewItemMouseClicked); + mBookmarksListView.mOnKeyDown.Add(new => BookmarksLV_OnKeyDown); + + AddWidget(mBookmarksListView); + + gApp.mBookmarkManager.BookmarksChanged.Add(new => BookmarksChanged); + gApp.mBookmarkManager.MovedToBookmark.Add(new => MovedToBookmark); + } + + private void BookmarksLV_OnKeyDown(KeyDownEvent event) + { + if (event.mKeyCode == KeyCode.Delete) + { + DeleteSelectedItems(); + } + + ListViewKeyDown_ShowMenu(event); + } + + public override void RehupScale(float oldScale, float newScale) + { + mBookmarksListView.mOpenButtonX = GS!(4); + + float iconButtonWidth = mBtnCreateBookmarkFolder.Width; + + mBtnPrevBookmark.X = GS!(1) + iconButtonWidth; + mBtnNextBookmark.X = (GS!(1) + iconButtonWidth) * 2; + mBtnPrevBookmarkInFolder.X = (GS!(1) + iconButtonWidth) * 3; + mBtnNextBookmarkInFolder.X = (GS!(1) + iconButtonWidth) * 4; + + base.RehupScale(oldScale, newScale); + } + + private void BookmarksLV_OnDragUpdate(DragEvent evt) + { + var dragKind = evt.mDragKind; + evt.mDragKind = .None; + + var dragSource = evt.mSender as BookmarksListViewItem; + var dragTarget = evt.mDragTarget as BookmarksListViewItem; + + // Folders can only be dragged onto other folders + if (dragSource.RefObject is BookmarkFolder && (!dragTarget.RefObject is BookmarkFolder)) + return; + + if (dragSource == null) + return; + if (dragTarget == null) + return; + + evt.mDragKind = .After; + + if ((dragTarget.mLabel == "") && (dragKind == .After)) + dragKind = .Before; + + if (dragKind == .None) + return; + evt.mDragKind = dragKind; + } + + private void BookmarksLV_OnDragEnd(DragEvent theEvent) + { + if (theEvent.mDragKind == .None) + return; + + if (theEvent.mDragTarget is BookmarksListViewItem) + { + var source = (BookmarksListViewItem)theEvent.mSender; + var target = (BookmarksListViewItem)theEvent.mDragTarget; + + if (source.mListView == target.mListView) + { + if (source == target) + return; + + List selectedItems = scope .(); + mBookmarksListView.GetRoot().WithSelectedItems(scope [&] (selectedItem) => + { + selectedItems.Add((BookmarksListViewItem)selectedItem); + }); + + for (BookmarksListViewItem item in selectedItems) + { + if (var sourceBookmark = item.RefObject as Bookmark) + { + if (var targetBookmark = target.RefObject as Bookmark) + { + if (theEvent.mDragKind == .After) + { + gApp.mBookmarkManager.MoveBookmarkToFolder(sourceBookmark, targetBookmark.mFolder, .After, targetBookmark); + } + else if (theEvent.mDragKind == .Before) + { + gApp.mBookmarkManager.MoveBookmarkToFolder(sourceBookmark, targetBookmark.mFolder, .Before, targetBookmark); + } + } + else if (var targetFolder = target.RefObject as BookmarkFolder) + { + if (theEvent.mDragKind == .Before) + { + // Drop before folder -> Drop to root + gApp.mBookmarkManager.MoveBookmarkToFolder(sourceBookmark, gApp.mBookmarkManager.mRootFolder); + } + else if (theEvent.mDragKind == .After || theEvent.mDragKind == .Inside) + { + gApp.mBookmarkManager.MoveBookmarkToFolder(sourceBookmark, targetFolder); + } + } + } + else if (var sourceFolder = item.RefObject as BookmarkFolder) + { + if (var targetFolder = target.RefObject as BookmarkFolder) + { + if (theEvent.mDragKind == .Before) + { + gApp.mBookmarkManager.MoveFolder(sourceFolder, .Before, targetFolder); + } + else if (theEvent.mDragKind == .After) + { + gApp.mBookmarkManager.MoveFolder(sourceFolder, .After, targetFolder); + } + } + } + } + } + } + } + + /// Tries to rename the currently selected bookmark + public void TryRenameItem() + { + ListViewItem selectedItem = mBookmarksListView.GetRoot().FindFirstSelectedItem(); + + RenameItem(selectedItem); + } + + private void HandleEditDone(EditWidget editWidget, bool cancelled) + { + String newValue = scope String(); + editWidget.GetText(newValue); + newValue.Trim(); + + ListViewItem listViewItem = mBookmarksListView.mEditingItem; + + if (var item = listViewItem as BookmarksListViewItem) + { + if (var bookmark = item.RefObject as Bookmark) + { + bookmark.mTitle.Clear(); + bookmark.mTitle.Append(newValue); + listViewItem.Label = bookmark.mTitle; + } + else if (var folder = item.RefObject as BookmarkFolder) + { + folder.mTitle.Clear(); + folder.mTitle.Append(newValue); + listViewItem.Label = folder.mTitle; + } + } + } + + protected override void ShowRightClickMenu(Widget relWidget, float x, float y) + { + base.ShowRightClickMenu(relWidget, x, y); + + var root = relWidget as ListViewItem; + var listView = root.mListView; + if (listView.GetRoot().FindFirstSelectedItem() != null) + { + Menu menu = new Menu(); + Menu anItem; + anItem = menu.AddItem("Delete"); + anItem.mOnMenuItemSelected.Add(new (item) => { DeleteSelectedItems(); }); + + anItem = menu.AddItem("Rename"); + anItem.mOnMenuItemSelected.Add(new (item) => { TryRenameItem(); }); + + menu.AddItem(); + + if (gApp.mBookmarkManager.AllBookmarksDisabled) + { + anItem = menu.AddItem("Enable all Bookmarks"); + anItem.mOnMenuItemSelected.Add(new (item) => + { + gApp.mBookmarkManager.AllBookmarksDisabled = false; + }); + } + else + { + anItem = menu.AddItem("Disable all Bookmarks"); + anItem.mOnMenuItemSelected.Add(new (item) => + { + gApp.mBookmarkManager.AllBookmarksDisabled = true; + }); + } + + MenuWidget menuWidget = ThemeFactory.mDefault.CreateMenuWidget(menu); + menuWidget.Init(relWidget, x, y); + } + } + + void EditListViewItem(ListViewItem listViewItem) + { + mBookmarksListView.EditListViewItem(listViewItem); + } + + void RenameItem(ListViewItem listViewItem) + { + if (listViewItem != null) + EditListViewItem(listViewItem); + } + + public override void Serialize(StructuredData data) + { + base.Serialize(data); + + data.Add("Type", "BookmarksPanel"); + } + + public override void Resize(float x, float y, float width, float height) + { + base.Resize(x, y, width, height); + + float buttonHeight = mBtnCreateBookmarkFolder.mHeight; + + mBookmarksListView.Resize(0, buttonHeight, width, Math.Max(mHeight - buttonHeight, 0)); + } + + private bool mBookmarksDirty; + + /// Marks the bookmarks list view as dirty so that it will be rebuild in the next update. + private void BookmarksChanged() + { + mBookmarksDirty = true; + } + + private void MovedToBookmark(Bookmark bookmark) + { + var root = (BookmarksListViewItem)mBookmarksListView.GetRoot(); + root.WithItems(scope (item) => + { + var bmItem = (BookmarksListViewItem)item; + + if (bmItem.RefObject == bookmark) + { + bmItem.mIsBold = true; + + ListViewItem parent = item.mParentItem; + parent.Open(true); + } + else + { + bmItem.mIsBold = false; + } + }); + } + + public override void Update() + { + if (mBookmarksDirty) + UpdateBookmarks(); + + ShowTooltip(mBtnCreateBookmarkFolder, "Create a new folder."); + ShowTooltip(mBtnPrevBookmark, "Move the cursor to the previous bookmark."); + ShowTooltip(mBtnNextBookmark, "Move the cursor to the next bookmark."); + ShowTooltip(mBtnPrevBookmarkInFolder, "Move the cursor to the previous bookmark in the current folder."); + ShowTooltip(mBtnNextBookmarkInFolder, "Move the cursor to the next bookmark in the current folder."); + + base.Update(); + } + + /// Clears the Panel (does NOT clear the actual bookmarks). + public void Clear() + { + mBookmarksListView.GetRoot().Clear(); + + mBookmarksDirty = true; + } + + /// Shows a tooltip with the given text for the specified widget if the widget is hovered. + private void ShowTooltip(Widget widget, String text) + { + if (DarkTooltipManager.CheckMouseover(widget, 20, let mousePoint)) + { + DarkTooltipManager.ShowTooltip(text, widget, mousePoint.x, mousePoint.y); + } + } + + /// Rebuilds the list view. + private void UpdateBookmarks() + { + var root = (BookmarksListViewItem)mBookmarksListView.GetRoot(); + + var openFolders = scope List(); + + root.WithItems(scope (item) => + { + if (item.IsOpen && (var bookmarkFolder = ((BookmarksListViewItem)item).RefObject as BookmarkFolder)) + { + openFolders.Add(bookmarkFolder); + } + }); + + root.Clear(); + + Bookmark currentBookmark = gApp.mBookmarkManager.CurrentBookmark; + + for (BookmarkFolder folder in gApp.mBookmarkManager.mBookmarkFolders) + { + bool isRoot = (folder == IDEApp.sApp.mBookmarkManager.mRootFolder); + + BookmarksListViewItem FolderItem = null; + + if (!isRoot) + { + FolderItem = AddFolderToListView(root, folder); + } + else + { + FolderItem = (BookmarksListViewItem)root; + } + + for (Bookmark bookmark in folder.mBookmarkList) + { + BookmarksListViewItem bmItem = AddBookmarkToListView(FolderItem, bookmark); + bmItem.mIsBold = (bookmark == currentBookmark); + } + + if (!isRoot) + { + // Open folder if it was open before recreating the list view. + int idx = openFolders.IndexOf(folder); + if (idx >= 0) + { + openFolders.RemoveAtFast(idx); + FolderItem.Open(true, true); + } + } + } + + mBookmarksDirty = false; + } + + /// Creates a new ListViewItem for the given folder. + private BookmarksListViewItem AddFolderToListView(BookmarksListViewItem parent, BookmarkFolder folder) + { + var listViewItem = (BookmarksListViewItem)parent.CreateChildItem(); + + listViewItem.RefObject = folder; + listViewItem.AllowDragging = true; + + var subViewItem = (DarkListViewItem)listViewItem.GetOrCreateSubItem(0); + + DarkCheckBox cb = new DarkCheckBox(); + cb.Checked = !folder.IsDisabled; + cb.Resize(GS!(-16), 0, GS!(22), GS!(22)); + cb.mOnValueChanged.Add(new () => + { + folder.IsDisabled = !cb.Checked; + }); + subViewItem.AddWidget(cb); + + subViewItem.Label = folder.mTitle; + subViewItem.Resize(GS!(22), 0, 0, 0); + + return listViewItem; + } + + /// Creates a new ListViewItem for the given bookmark. + private BookmarksListViewItem AddBookmarkToListView(BookmarksListViewItem parent, Bookmark bookmark) + { + var listViewItem = (BookmarksListViewItem)(parent.CreateChildItem()); + + listViewItem.RefObject = bookmark; + listViewItem.AllowDragging = true; + + var subViewItem = (DarkListViewItem)listViewItem.GetOrCreateSubItem(0); + + DarkCheckBox cb = new DarkCheckBox(); + cb.Checked = !bookmark.mIsDisabled; + cb.Resize(GS!(-16), 0, GS!(22), GS!(22)); + cb.mOnValueChanged.Add(new () => + { + bookmark.mIsDisabled = !cb.Checked; + }); + subViewItem.AddWidget(cb); + + subViewItem.Label = bookmark.mTitle; + subViewItem.Resize(GS!(22), 0, 0, 0); + + subViewItem = (DarkListViewItem)listViewItem.GetOrCreateSubItem(1); + subViewItem.Label = bookmark.mFileName; + + // Internally lines are 0-based -> add one for display + listViewItem.BookmarkLine = new $"{bookmark.mLineNum + 1}"; + + subViewItem = (DarkListViewItem)listViewItem.GetOrCreateSubItem(2); + subViewItem.Label = listViewItem.BookmarkLine; + + return listViewItem; + } + + public override void KeyDown(KeyCode keyCode, bool isRepeat) + { + mBookmarksListView.KeyDown(keyCode, isRepeat); + + base.KeyDown(keyCode, isRepeat); + } + + private void DeleteSelectedItems() + { + var root = mBookmarksListView.GetRoot(); + List selectedItems = scope List(); + root.WithSelectedItems(scope (listViewItem) => + { + selectedItems.Add(listViewItem); + }); + + // Go through in reverse, to process children before their parents + for (int itemIdx = selectedItems.Count - 1; itemIdx >= 0; itemIdx--) + { + BookmarksListViewItem item = (.)selectedItems[itemIdx]; + + if (var bookmark = item.RefObject as Bookmark) + gApp.mBookmarkManager.DeleteBookmark(bookmark); + else if (var folder = item.RefObject as BookmarkFolder) + gApp.mBookmarkManager.DeleteFolder(folder); + } + } + } +} diff --git a/IDE/src/ui/Panel.bf b/IDE/src/ui/Panel.bf index 964e2f3d..57ad9e18 100644 --- a/IDE/src/ui/Panel.bf +++ b/IDE/src/ui/Panel.bf @@ -155,6 +155,10 @@ namespace IDE.ui { panel = gApp.mModulePanel; } + else if (type == "BookmarksPanel") + { + panel = gApp.mBookmarksPanel; + } if (panel != null) { diff --git a/IDE/src/ui/SourceViewPanel.bf b/IDE/src/ui/SourceViewPanel.bf index 09fcbefe..1112ce42 100644 --- a/IDE/src/ui/SourceViewPanel.bf +++ b/IDE/src/ui/SourceViewPanel.bf @@ -3199,16 +3199,19 @@ namespace IDE.ui mTrackedTextElementViewList = new List(); if (mFilePath == null) return mTrackedTextElementViewList; - - for (var bookmark in IDEApp.sApp.mBookmarkManager.mBookmarkList) - { - if (Path.Equals(bookmark.mFileName, findFileName)) - { - var bookmarkView = new TrackedTextElementView(bookmark); - UpdateTrackedElementView(bookmarkView); - mTrackedTextElementViewList.Add(bookmarkView); - } - } + + for (var folder in IDEApp.sApp.mBookmarkManager.mBookmarkFolders) + { + for (var bookmark in folder.mBookmarkList) + { + if (Path.Equals(bookmark.mFileName, findFileName)) + { + var bookmarkView = new TrackedTextElementView(bookmark); + UpdateTrackedElementView(bookmarkView); + mTrackedTextElementViewList.Add(bookmarkView); + } + } + } for (var breakpoint in debugManager.mBreakpointList) { @@ -4421,11 +4424,14 @@ namespace IDE.ui bool hadBookmark = false; - WithTrackedElementsAtCursor(IDEApp.sApp.mBookmarkManager.mBookmarkList, scope [&] (bookmark) => - { - bookmarkManager.DeleteBookmark(bookmark); - hadBookmark = true; - }); + for (var folder in IDEApp.sApp.mBookmarkManager.mBookmarkFolders) + { + WithTrackedElementsAtCursor(folder.mBookmarkList, scope [&] (bookmark) => + { + bookmarkManager.DeleteBookmark(bookmark); + hadBookmark = true; + }); + } if (!hadBookmark) { @@ -4670,7 +4676,8 @@ namespace IDE.ui if (ewc.IsLineCollapsed(drawLineNum)) continue; //hadLineIcon[drawLineNum - lineStart] = true; - g.Draw(DarkTheme.sDarkTheme.GetImage(.IconBookmark), Math.Max(GS!(-5), mEditWidget.mX - GS!(30) - sDrawLeftAdjust), + Image image = DarkTheme.sDarkTheme.GetImage(bookmark.mIsDisabled ? .IconBookmarkDisabled : .IconBookmark); + g.Draw(image, Math.Max(GS!(-5), mEditWidget.mX - GS!(30) - sDrawLeftAdjust), 0 + bookmark.mLineNum * lineSpacing); var curLineFlags = ref lineFlags[drawLineNum - lineStart];