From 1bc89b12dd1a1df8ba8a8d98cd8e4798ea8f25e4 Mon Sep 17 00:00:00 2001 From: Ansgar Becker Date: Tue, 19 Aug 2025 06:27:10 +0200 Subject: [PATCH] Issue #1525: remove project manager feature with all its unrelated changes --- out/locale/en/LC_MESSAGES/default.po | 92 -- packages/Delphi12.1/heidisql.dpr | 3 +- packages/Delphi12.1/heidisql.dproj | 1 - packages/Delphi12.3/heidisql.dpr | 3 +- packages/Delphi12.3/heidisql.dproj | 1 - source/apphelpers.pas | 13 +- source/main.dfm | 31 +- source/main.pas | 506 +------- source/projectmanager.dfm | 152 --- source/projectmanager.pas | 1715 -------------------------- 10 files changed, 38 insertions(+), 2479 deletions(-) delete mode 100644 source/projectmanager.dfm delete mode 100644 source/projectmanager.pas diff --git a/out/locale/en/LC_MESSAGES/default.po b/out/locale/en/LC_MESSAGES/default.po index 573bfc51..52982389 100644 --- a/out/locale/en/LC_MESSAGES/default.po +++ b/out/locale/en/LC_MESSAGES/default.po @@ -6768,95 +6768,3 @@ msgstr "Select top %s rows" msgid "Selects the first %s rows in a new query tab" msgstr "Selects the first %s rows in a new query tab" - -#. Project Manager -msgid "Project Manager" -msgstr "Project Manager" - -msgid "Project &Manager" -msgstr "Project &Manager" - -msgid "Toggle Project Manager tab in query helpers (F11)" -msgstr "Toggle Project Manager tab in query helpers (F11)" - -#. Project Manager - Button hints -msgid "Add Project Folder" -msgstr "Add Project Folder" - -msgid "Remove Project Folder" -msgstr "Remove Project Folder" - -msgid "Rename Project" -msgstr "Rename Project" - -msgid "Refresh Projects" -msgstr "Refresh Projects" - -#. Project Manager - Context menu -msgid "Open File" -msgstr "Open File" - -msgid "Open with System Editor" -msgstr "Open with System Editor" - -msgid "Open Folder" -msgstr "Open Folder" - -msgid "Copy Path" -msgstr "Copy Path" - -msgid "Add Project Folder..." -msgstr "Add Project Folder..." - -msgid "Remove Project" -msgstr "Remove Project" - -#. Project Manager - Dialogs -msgid "Select Project Folder" -msgstr "Select Project Folder" - -msgid "Root" -msgstr "Root" - -msgid "Project Name" -msgstr "Project Name" - -msgid "Enter a name for this project:" -msgstr "Enter a name for this project:" - -#. Project Manager - Error messages -msgid "Error in CreateWnd: " -msgstr "Error in CreateWnd: " - -msgid "Project name cannot be empty!" -msgstr "Project name cannot be empty!" - -msgid "A project with this name already exists!" -msgstr "A project with this name already exists!" - -msgid "This folder is already added as a project!" -msgstr "This folder is already added as a project!" - -msgid "Please select a project to remove." -msgstr "Please select a project to remove." - -msgid "Project not found in list." -msgstr "Project not found in list." - -msgid "Please select a project to rename." -msgstr "Please select a project to rename." - -msgid "Database files should be opened via the connection manager." -msgstr "Database files should be opened via the connection manager." - -msgid "Use File > Connect to Database to open this SQLite database." -msgstr "Use File > Connect to Database to open this SQLite database." - -msgid "Failed to copy path to clipboard: " -msgstr "Failed to copy path to clipboard: " - -msgid "Error saving projects: " -msgstr "Error saving projects: " - -msgid "Error opening file: " -msgstr "Error opening file: " diff --git a/packages/Delphi12.1/heidisql.dpr b/packages/Delphi12.1/heidisql.dpr index 4fb1b46e..b9d721e6 100644 --- a/packages/Delphi12.1/heidisql.dpr +++ b/packages/Delphi12.1/heidisql.dpr @@ -58,8 +58,7 @@ uses customize_highlighter in '..\..\source\customize_highlighter.pas' {frmCustomizeHighlighter}, Xml.VerySimple in '..\..\source\Xml.VerySimple.pas', Sequal.Suggest in '..\..\source\Sequal.Suggest.pas' {SequalSuggestForm}, - reformatter in '..\..\source\reformatter.pas' {frmReformatter}, - projectmanager in '..\..\source\projectmanager.pas'; + reformatter in '..\..\source\reformatter.pas' {frmReformatter}; {.$R *.RES} {$R ..\..\res\icon.RES} diff --git a/packages/Delphi12.1/heidisql.dproj b/packages/Delphi12.1/heidisql.dproj index 632380f3..49289f45 100644 --- a/packages/Delphi12.1/heidisql.dproj +++ b/packages/Delphi12.1/heidisql.dproj @@ -265,7 +265,6 @@
frmReformatter
dfm - Base diff --git a/packages/Delphi12.3/heidisql.dpr b/packages/Delphi12.3/heidisql.dpr index 4fb1b46e..b9d721e6 100644 --- a/packages/Delphi12.3/heidisql.dpr +++ b/packages/Delphi12.3/heidisql.dpr @@ -58,8 +58,7 @@ uses customize_highlighter in '..\..\source\customize_highlighter.pas' {frmCustomizeHighlighter}, Xml.VerySimple in '..\..\source\Xml.VerySimple.pas', Sequal.Suggest in '..\..\source\Sequal.Suggest.pas' {SequalSuggestForm}, - reformatter in '..\..\source\reformatter.pas' {frmReformatter}, - projectmanager in '..\..\source\projectmanager.pas'; + reformatter in '..\..\source\reformatter.pas' {frmReformatter}; {.$R *.RES} {$R ..\..\res\icon.RES} diff --git a/packages/Delphi12.3/heidisql.dproj b/packages/Delphi12.3/heidisql.dproj index 12c6fa7b..a725978f 100644 --- a/packages/Delphi12.3/heidisql.dproj +++ b/packages/Delphi12.3/heidisql.dproj @@ -267,7 +267,6 @@
frmReformatter
dfm
- Base diff --git a/source/apphelpers.pas b/source/apphelpers.pas index 1be036d6..5dd2ffda 100644 --- a/source/apphelpers.pas +++ b/source/apphelpers.pas @@ -182,7 +182,7 @@ type asMaxColWidth, asDatagridMaximumRows, asDatagridRowsPerStep, asGridRowLineCount, asColumnHeaderClick, asReuseEditorConfiguration, asLogToFile, asMainWinMaximized, asMainWinLeft, asMainWinTop, asMainWinWidth, asMainWinHeight, asMainWinOnMonitor, asCoolBandIndex, asCoolBandBreak, asCoolBandWidth, asToolbarShowCaptions, asQuerymemoheight, asDbtreewidth, - asDataPreviewHeight, asDataPreviewEnabled, asLogHeight, asQueryhelperswidth, asProjectManagerWidth, asProjectManagerVisible, asProjectManagerTabActive, asStopOnErrorsInBatchMode, + asDataPreviewHeight, asDataPreviewEnabled, asLogHeight, asQueryhelperswidth, asStopOnErrorsInBatchMode, asWrapLongLines, asCodeFolding, asDisplayBLOBsAsText, asSingleQueries, asMemoEditorWidth, asMemoEditorHeight, asMemoEditorMaximized, asMemoEditorWrap, asMemoEditorHighlighter, asMemoEditorAlwaysFormatCode, asDelimiter, asSQLHelpWindowLeft, asSQLHelpWindowTop, asSQLHelpWindowWidth, asSQLHelpWindowHeight, asSQLHelpPnlLeftWidth, asSQLHelpPnlRightTopHeight, asHost, @@ -236,7 +236,6 @@ type asSequalSuggestWindowWidth, asSequalSuggestWindowHeight, asSequalSuggestPrompt, asSequalSuggestRecentPrompts, asReformatter, asReformatterNoDialog, asAlwaysGenerateFilter, asGenerateDataNumRows, asGenerateDataNullAmount, asWebOnceAction, - asProjectManagerHeight, asUnused); TAppSetting = record Name: String; @@ -528,10 +527,10 @@ begin Exit; if FromLeft then begin SetLength(Result, MaxLen); - Result[MaxLen] := '.'; + Result[MaxLen] := '…'; end else begin Result := Copy(Result, Length(Result)-MaxLen, Length(Result)); - Result := '…' + Result; + Result := '…' + Result; end; end; @@ -854,7 +853,7 @@ end; function RoundCommercial(e: Extended): Int64; begin - // "Kaufmännisch runden" + // "Kaufmännisch runden" // In contrast to Delphi's Round() which rounds *.5 to the next even number Result := Trunc(e); if Frac(e) >= 0.5 then @@ -3793,9 +3792,6 @@ begin InitSetting(asDataPreviewEnabled, 'DataPreviewEnabled', 0, False); InitSetting(asLogHeight, 'sqloutheight', 80); InitSetting(asQueryhelperswidth, 'queryhelperswidth', 200); - InitSetting(asProjectManagerWidth, 'projectmanagerwidth', 260); - InitSetting(asProjectManagerVisible, 'projectmanagervisible', 0, True); - InitSetting(asProjectManagerTabActive, 'projectmanagertabactive', 0, False); InitSetting(asStopOnErrorsInBatchMode, 'StopOnErrorsInBatchMode', 0, True); InitSetting(asWrapLongLines, 'WrapLongLines', 0, False); InitSetting(asCodeFolding, 'CodeFolding', 0, True); @@ -4056,7 +4052,6 @@ begin InitSetting(asCreateDbCollation, 'CreateDbCollation', 0, False, ''); InitSetting(asRealTrailingZeros, 'RealTrailingZeros', 1); InitSetting(asWebOnceAction, 'WebOnceAction', 0, False, DateToStr(DateTimeNever)); - InitSetting(asProjectManagerHeight, 'ProjectManagerHeight', 200); // Initialization values FRestoreTabsInitValue := ReadBool(asRestoreTabs); diff --git a/source/main.dfm b/source/main.dfm index a5aa5ca7..e963f11e 100644 --- a/source/main.dfm +++ b/source/main.dfm @@ -155,7 +155,6 @@ object MainForm: TMainForm DragMode = dmAutomatic DragType = dtVCL Header.AutoSizeIndex = 0 - Header.Height = 18 Header.Options = [hoAutoResize, hoColumnResize, hoDrag] HintMode = hmTooltip HotCursor = crHandPoint @@ -191,7 +190,7 @@ object MainForm: TMainForm item Position = 0 Text = 'Name' - Width = 169 + Width = 165 end item Alignment = taRightJustify @@ -432,7 +431,6 @@ object MainForm: TMainForm Height = 195 Align = alClient Header.AutoSizeIndex = 0 - Header.Height = 18 Header.Options = [hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoShowSortGlyphs, hoVisible, hoDisableAnimatedResize, hoAutoResizeInclCaption] Header.PopupMenu = popupListHeader Header.SortColumn = 0 @@ -516,7 +514,7 @@ object MainForm: TMainForm Align = alClient DragOperations = [] Header.AutoSizeIndex = 2 - Header.Height = 18 + Header.Height = 20 Header.Options = [hoAutoResize, hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoShowSortGlyphs, hoVisible, hoDisableAnimatedResize, hoAutoResizeInclCaption] Header.PopupMenu = popupListHeader Header.SortColumn = 0 @@ -559,7 +557,7 @@ object MainForm: TMainForm item Position = 2 Text = 'Global' - Width = 316 + Width = 312 end> end end @@ -574,7 +572,7 @@ object MainForm: TMainForm Align = alClient DragOperations = [] Header.AutoSizeIndex = 1 - Header.Height = 18 + Header.Height = 20 Header.Options = [hoAutoResize, hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoShowSortGlyphs, hoVisible, hoDisableAnimatedResize, hoAutoResizeInclCaption] Header.PopupMenu = popupListHeader Header.SortColumn = 0 @@ -611,7 +609,7 @@ object MainForm: TMainForm Alignment = taRightJustify Position = 1 Text = 'Value' - Width = 316 + Width = 312 end item Alignment = taRightJustify @@ -646,7 +644,7 @@ object MainForm: TMainForm Height = 122 Align = alClient Header.AutoSizeIndex = 7 - Header.Height = 18 + Header.Height = 20 Header.Options = [hoAutoResize, hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoShowSortGlyphs, hoVisible, hoDisableAnimatedResize, hoAutoResizeInclCaption] Header.PopupMenu = popupListHeader Header.SortColumn = 0 @@ -713,7 +711,7 @@ object MainForm: TMainForm item Position = 7 Text = 'Info' - Width = 186 + Width = 182 end> end object pnlProcessViewBox: TPanel @@ -795,7 +793,7 @@ object MainForm: TMainForm Height = 195 Align = alClient Header.AutoSizeIndex = 4 - Header.Height = 18 + Header.Height = 20 Header.Options = [hoAutoResize, hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoShowSortGlyphs, hoVisible, hoDisableAnimatedResize, hoAutoResizeInclCaption] Header.PopupMenu = popupListHeader Header.SortColumn = 1 @@ -850,7 +848,7 @@ object MainForm: TMainForm item Position = 4 Text = 'Percentage' - Width = 256 + Width = 252 end> end end @@ -867,7 +865,7 @@ object MainForm: TMainForm Align = alClient EditDelay = 500 Header.AutoSizeIndex = -1 - Header.Height = 18 + Header.Height = 20 Header.Options = [hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoShowSortGlyphs, hoVisible, hoDisableAnimatedResize, hoAutoResizeInclCaption] Header.PopupMenu = popupListHeader Header.SortColumn = 0 @@ -1029,8 +1027,6 @@ object MainForm: TMainForm Caption = 'No data available for this item.' Layout = tlCenter WordWrap = True - ExplicitWidth = 165 - ExplicitHeight = 14 end object pnlDataTop: TPanel Left = 0 @@ -1242,7 +1238,7 @@ object MainForm: TMainForm AutoScrollDelay = 50 EditDelay = 0 Header.AutoSizeIndex = -1 - Header.Height = 14 + Header.Height = 20 Header.Images = VirtualImageListMain Header.MainColumn = -1 Header.Options = [hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoOwnerDraw, hoShowHint, hoShowImages, hoVisible, hoDisableAnimatedResize, hoAutoResizeInclCaption] @@ -1404,7 +1400,6 @@ object MainForm: TMainForm DragMode = dmAutomatic DragType = dtVCL Header.AutoSizeIndex = 0 - Header.Height = 18 Header.Options = [hoAutoResize, hoColumnResize, hoDrag, hoShowSortGlyphs] Images = VirtualImageListMain IncrementalSearch = isVisibleOnly @@ -1437,7 +1432,7 @@ object MainForm: TMainForm item Position = 0 Text = 'Main column' - Width = 85 + Width = 64 end item Position = 1 @@ -1471,7 +1466,7 @@ object MainForm: TMainForm AutoScrollDelay = 50 EditDelay = 0 Header.AutoSizeIndex = -1 - Header.Height = 14 + Header.Height = 20 Header.Images = VirtualImageListMain Header.MainColumn = -1 Header.Options = [hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoOwnerDraw, hoShowHint, hoShowImages, hoDisableAnimatedResize, hoAutoResizeInclCaption] diff --git a/source/main.pas b/source/main.pas index 5f055283..81586758 100644 --- a/source/main.pas +++ b/source/main.pas @@ -16,7 +16,7 @@ uses Winapi.CommCtrl, System.Contnrs, System.Generics.Collections, System.Generics.Defaults, SynEditExport, SynExportHTML, SynExportRTF, System.Math, Vcl.ExtDlgs, System.Win.Registry, Vcl.AppEvnts, routine_editor, trigger_editor, event_editor, preferences, EditVar, apphelpers, createdatabase, table_editor, TableTools, View, Usermanager, SelectDBObject, connections, sqlhelp, dbconnection, - insertfiles, searchreplace, loaddata, copytable, csv_detector, Cromis.DirectoryWatch, SyncDB, gnugettext, projectmanager, + insertfiles, searchreplace, loaddata, copytable, csv_detector, Cromis.DirectoryWatch, SyncDB, gnugettext, VirtualTrees, VirtualTrees.HeaderPopup, VirtualTrees.Utils, VirtualTrees.Types, JumpList, System.Actions, System.UITypes, Vcl.Imaging.pngimage, System.ImageList, Vcl.Styles.Utils.Forms, @@ -102,10 +102,6 @@ type pnlMemo: TPanel; Memo: TSynMemo; pnlHelpers: TPanel; - pcHelpers: TPageControl; // PageControl for Helpers and Project Manager - tabHelpers: TTabSheet; // Tab for the original Helpers - tabProjectManager: TTabSheet; // Tab for the Project Manager - projectManagerPanel: TProjectManagerPanel; // Project Manager Instance pro Tab filterHelpers: TButtonedEdit; treeHelpers: TVirtualStringTree; MemoFileRenamed: Boolean; @@ -507,7 +503,6 @@ type pnlRight: TPanel; btnCloseFilterPanel: TSpeedButton; actFilterPanel: TAction; - actProjectManager: TAction; actFindInVT1: TMenuItem; TimerFilterVT: TTimer; actFindTextOnServer: TAction; @@ -800,7 +795,6 @@ type actGenerateData: TAction; Generatedata1: TMenuItem; Generatedata2: TMenuItem; - menuProjectManager: TMenuItem; actCopyGridNodes: TAction; actCopyGridNodes1: TMenuItem; actQueryTable: TAction; @@ -1003,9 +997,6 @@ type procedure actCloseQueryTabExecute(Sender: TObject); procedure menuCloseQueryTabClick(Sender: TObject); procedure CloseQueryTab(PageIndex: Integer); - function GetQueryTabBackupFilename(Tab: TQueryTab): String; - procedure SaveQueryTabBackupToIni(Tab: TQueryTab; const BackupFilename: String); - function ShouldCreateQueryTabBackup(Tab: TQueryTab): Boolean; procedure CloseButtonOnMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure CloseButtonOnMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); function GetMainTabAt(X, Y: Integer): Integer; @@ -1016,7 +1007,6 @@ type procedure popupMainTabsPopup(Sender: TObject); procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); procedure actFilterPanelExecute(Sender: TObject); - procedure actProjectManagerExecute(Sender: TObject); procedure TimerFilterVTTimer(Sender: TObject); procedure PageControlMainContextPopup(Sender: TObject; MousePos: TPoint; var Handled: Boolean); procedure menuQueryHelpersGenerateStatementClick(Sender: TObject); @@ -1212,14 +1202,7 @@ type var HintText: string); procedure actCopyGridNodesExecute(Sender: TObject); procedure actQueryTableExecute(Sender: TObject); - procedure ToggleProjectManager; // Toggle Project Manager Panel visibility - procedure ApplyProjectManagerStateToActiveTab; // Apply global Project Manager state to active tab - procedure HelpersPageControlChange(Sender: TObject); // Handle manual tab changes in helpers PageControl - procedure SynchronizeAllQueryTabsToGlobalState(ExcludeTab: TQueryTab = nil); // Sync all tabs to global state private - // Project Manager global state - FProjectManagerTabActive: Boolean; // Global state: should Project Manager tab be active? - // Executable file details FAppVerMajor: Integer; FAppVerMinor: Integer; @@ -1328,7 +1311,6 @@ type procedure StoreTabs; function RestoreTabs: Boolean; procedure SetHintFontByControl(Control: TWinControl=nil); - private public QueryTabs: TQueryTabList; ActiveObjectEditor: TDBObjectEditor; @@ -1870,7 +1852,6 @@ begin AppSettings.WriteBool(asDataPreviewEnabled, actDataPreview.Checked); AppSettings.WriteInt(asLogHeight, SynMemoSQLLog.Height); AppSettings.WriteBool(asFilterPanel, actFilterPanel.Checked); - AppSettings.WriteBool(asProjectManagerTabActive, FProjectManagerTabActive); AppSettings.WriteBool(asWrapLongLines, actQueryWordWrap.Checked); AppSettings.WriteBool(asCodeFolding, actCodeFolding.Checked); AppSettings.WriteBool(asSingleQueries, actSingleQueries.Checked); @@ -2027,54 +2008,6 @@ begin QueryTab.Uid := TQueryTab.GenerateUid; QueryTab.pnlMemo := pnlQueryMemo; QueryTab.pnlHelpers := pnlQueryHelpers; - - // Convert the first tab to use the new PageControl system - // Create PageControl for the existing pnlQueryHelpers - QueryTab.pcHelpers := TPageControl.Create(QueryTab.pnlHelpers); - QueryTab.pcHelpers.Name := 'pcHelpers0'; - QueryTab.pcHelpers.Parent := QueryTab.pnlHelpers; - QueryTab.pcHelpers.Align := alClient; - QueryTab.pcHelpers.TabPosition := tpTop; - QueryTab.pcHelpers.Style := tsButtons; - QueryTab.pcHelpers.OnChange := HelpersPageControlChange; // Event handler for tab changes - - // Create "Helpers" tab and move existing components - QueryTab.tabHelpers := TTabSheet.Create(QueryTab.pcHelpers); - QueryTab.tabHelpers.Name := 'tabHelpers0'; - QueryTab.tabHelpers.Caption := 'Helpers'; - QueryTab.tabHelpers.PageControl := QueryTab.pcHelpers; - - // Move existing components to the Helpers tab - filterQueryHelpers.Parent := QueryTab.tabHelpers; - treeQueryHelpers.Parent := QueryTab.tabHelpers; - - // Create "Project Manager" tab - QueryTab.tabProjectManager := TTabSheet.Create(QueryTab.pcHelpers); - QueryTab.tabProjectManager.Name := 'tabProjectManager0'; - QueryTab.tabProjectManager.Caption := 'Project Manager'; - QueryTab.tabProjectManager.PageControl := QueryTab.pcHelpers; - - // Create Project Manager in the Project Manager tab - try - QueryTab.projectManagerPanel := TProjectManagerPanel.Create(QueryTab.tabProjectManager); - QueryTab.projectManagerPanel.Parent := QueryTab.tabProjectManager; - QueryTab.projectManagerPanel.Align := alClient; - LogSQL('Project Manager panel created successfully in main query tab', lcDebug); - except - on E: Exception do - begin - LogSQL('Error creating Project Manager in main query tab: ' + E.Message, lcError); - QueryTab.projectManagerPanel := nil; - end; - end; - - // Set initial active tab based on global Project Manager state (loaded from settings) - FProjectManagerTabActive := AppSettings.ReadBool(asProjectManagerTabActive, '', False); - if FProjectManagerTabActive then - QueryTab.pcHelpers.ActivePage := QueryTab.tabProjectManager - else - QueryTab.pcHelpers.ActivePage := QueryTab.tabHelpers; - QueryTab.filterHelpers := filterQueryHelpers; QueryTab.treeHelpers := treeQueryHelpers; QueryTab.Memo := SynMemoQuery; @@ -2255,33 +2188,6 @@ begin LogSQL(f_('Theme: "%s"', [TStyleManager.ActiveStyle.Name]), lcDebug); LogSQL(f_('Pixels per inch on current monitor: %d', [Monitor.PixelsPerInch]), lcDebug); LogSQL(f_('Timezone offset: %d', [FTimeZoneOffset]), lcDebug); - - // Create and configure Project Manager action for tab switching - try - if not Assigned(actProjectManager) then - begin - actProjectManager := TAction.Create(Self); - actProjectManager.ActionList := ActionList1; - actProjectManager.Category := 'View'; - actProjectManager.Name := 'actProjectManager'; - end; - - if Assigned(actProjectManager) then - begin - actProjectManager.Caption := _('Project &Manager'); - actProjectManager.Hint := _('Toggle Project Manager tab in query helpers (F11)'); - actProjectManager.ShortCut := VK_F11; - actProjectManager.OnExecute := actProjectManagerExecute; - actProjectManager.Checked := FProjectManagerTabActive; // Reflect global state - - LogSQL('Project Manager action configured for tab switching', lcDebug); - end; - except - on E: Exception do - begin - LogSQL('Error creating Project Manager action: ' + E.Message, lcError); - end; - end; end; @@ -6455,11 +6361,6 @@ begin end; end; - // Apply global Project Manager state to newly active query tab (always, not just on manual clicks) - if IsQueryTab(tab.PageIndex, True) then begin - ApplyProjectManagerStateToActiveTab; - end; - // Filter panel has one text per tab, which we need to update UpdateFilterPanel(Sender); @@ -11969,29 +11870,6 @@ begin end; -procedure TMainForm.ToggleProjectManager; -begin - // Toggle global Project Manager state - FProjectManagerTabActive := not FProjectManagerTabActive; - - {$IFDEF DEBUG} - LogSQL(Format('Toggled Project Manager state to: %s', [BoolToStr(FProjectManagerTabActive, True)]), lcDebug); - {$ENDIF} - - // Apply the state to all existing query tabs - SynchronizeAllQueryTabsToGlobalState; - - // Update action checked state - if Assigned(actProjectManager) then - actProjectManager.Checked := FProjectManagerTabActive; - - // Show status message - if FProjectManagerTabActive then - ShowStatusMsg('Project Manager activated for all query tabs') - else - ShowStatusMsg('Helpers activated for all query tabs'); -end; - procedure TMainForm.actCopyGridNodesExecute(Sender: TObject); var SenderControl: TComponent; @@ -12575,32 +12453,10 @@ begin else QueryTab.pnlHelpers.Width := AppSettings.GetDefaultInt(asQueryhelperswidth); - // Create PageControl for Helpers and Project Manager tabs - QueryTab.pcHelpers := TPageControl.Create(QueryTab.pnlHelpers); - QueryTab.pcHelpers.Name := 'pcHelpers' + i.ToString; - QueryTab.pcHelpers.Parent := QueryTab.pnlHelpers; - QueryTab.pcHelpers.Align := alClient; - QueryTab.pcHelpers.TabPosition := tpTop; - QueryTab.pcHelpers.Style := tsButtons; - QueryTab.pcHelpers.OnChange := HelpersPageControlChange; // Event handler for tab changes - - // Create "Helpers" tab - QueryTab.tabHelpers := TTabSheet.Create(QueryTab.pcHelpers); - QueryTab.tabHelpers.Name := 'tabHelpers' + i.ToString; - QueryTab.tabHelpers.Caption := 'Helpers'; - QueryTab.tabHelpers.PageControl := QueryTab.pcHelpers; - - // Create "Project Manager" tab - QueryTab.tabProjectManager := TTabSheet.Create(QueryTab.pcHelpers); - QueryTab.tabProjectManager.Name := 'tabProjectManager' + i.ToString; - QueryTab.tabProjectManager.Caption := 'Project Manager'; - QueryTab.tabProjectManager.PageControl := QueryTab.pcHelpers; - - // Move filter and tree to Helpers tab - QueryTab.filterHelpers := TButtonedEdit.Create(QueryTab.tabHelpers); + QueryTab.filterHelpers := TButtonedEdit.Create(QueryTab.pnlHelpers); QueryTab.filterHelpers.Name := filterQueryHelpers.Name + i.ToString; QueryTab.filterHelpers.Text := ''; - QueryTab.filterHelpers.Parent := QueryTab.tabHelpers; + QueryTab.filterHelpers.Parent := QueryTab.pnlHelpers; QueryTab.filterHelpers.Align := filterQueryHelpers.Align; QueryTab.filterHelpers.TextHint := filterQueryHelpers.TextHint; QueryTab.filterHelpers.Images := filterQueryHelpers.Images; @@ -12611,9 +12467,9 @@ begin QueryTab.filterHelpers.OnChange := filterQueryHelpers.OnChange; QueryTab.filterHelpers.OnRightButtonClick := filterQueryHelpers.OnRightButtonClick; - QueryTab.treeHelpers := TVirtualStringTree.Create(QueryTab.tabHelpers); + QueryTab.treeHelpers := TVirtualStringTree.Create(QueryTab.pnlHelpers); QueryTab.treeHelpers.Name := treeQueryHelpers.Name + i.ToString; - QueryTab.treeHelpers.Parent := QueryTab.tabHelpers; + QueryTab.treeHelpers.Parent := QueryTab.pnlHelpers; QueryTab.treeHelpers.Align := treeQueryHelpers.Align; QueryTab.treeHelpers.Left := treeQueryHelpers.Left; QueryTab.treeHelpers.Constraints.MinWidth := treeQueryHelpers.Constraints.MinWidth; @@ -12649,28 +12505,6 @@ begin QueryTab.treeHelpers.TextMargin := treeQueryHelpers.TextMargin; FixVT(QueryTab.treeHelpers); - // Create Project Manager in Project Manager tab - try - QueryTab.projectManagerPanel := TProjectManagerPanel.Create(QueryTab.tabProjectManager); - QueryTab.projectManagerPanel.Parent := QueryTab.tabProjectManager; - QueryTab.projectManagerPanel.Align := alClient; - - // Initialize Project Manager if needed - LogSQL('Project Manager panel created successfully in query tab ' + i.ToString, lcDebug); - except - on E: Exception do - begin - LogSQL('Error creating Project Manager in query tab: ' + E.Message, lcError); - QueryTab.projectManagerPanel := nil; - end; - end; - - // Set active tab based on global Project Manager state - if FProjectManagerTabActive then - QueryTab.pcHelpers.ActivePage := QueryTab.tabProjectManager - else - QueryTab.pcHelpers.ActivePage := QueryTab.tabHelpers; - QueryTab.spltQuery := TSplitter.Create(QueryTab.TabSheet); QueryTab.spltQuery.Parent := QueryTab.TabSheet; QueryTab.spltQuery.Top := spltQuery.Top; // Important to get it below the editor, not above. See #439 @@ -12868,8 +12702,6 @@ procedure TMainForm.CloseQueryTab(PageIndex: Integer); var NewPageIndex: Integer; Grid: TVirtualStringTree; - QueryTab: TQueryTab; - BackupFilename: String; begin // Special case: the very first tab gets cleared but not closed if PageIndex = tabQuery.PageIndex then begin @@ -12878,30 +12710,6 @@ begin end; if not IsQueryTab(PageIndex, False) then Exit; - - // Get the query tab before we destroy it - QueryTab := QueryTabs.TabByControl(PageControlMain.Pages[PageIndex]); - - // Create automatic backup if needed - if Assigned(QueryTab) and ShouldCreateQueryTabBackup(QueryTab) then - begin - try - BackupFilename := GetQueryTabBackupFilename(QueryTab); - if BackupFilename <> '' then - begin - QueryTab.Memo.Lines.SaveToFile(BackupFilename, TEncoding.UTF8); - SaveQueryTabBackupToIni(QueryTab, BackupFilename); - LogSQL(f_('Query tab content backed up to: %s', [BackupFilename]), lcInfo); - end; - except - on E: Exception do - begin - // Don't prevent tab closing due to backup errors, but log them - LogSQL(f_('Error creating query tab backup: %s', [E.Message]), lcError); - end; - end; - end; - // Cancel cell editor if active, preventing crash. See issue #2040 Grid := ActiveGrid; if Assigned(Grid) and Grid.IsEditing then @@ -12917,14 +12725,11 @@ begin Dec(NewPageIndex); // Avoid excessive flicker: LockWindowUpdate(PageControlMain.Handle); - try - PageControlMain.Pages[PageIndex].Free; - QueryTabs.Delete(PageIndex-tabQuery.PageIndex); - PageControlMain.ActivePageIndex := NewPageIndex; - FixQueryTabCloseButtons; - finally - LockWindowUpdate(0); - end; + PageControlMain.Pages[PageIndex].Free; + QueryTabs.Delete(PageIndex-tabQuery.PageIndex); + PageControlMain.ActivePageIndex := NewPageIndex; + FixQueryTabCloseButtons; + LockWindowUpdate(0); PageControlMain.OnChange(PageControlMain); end; @@ -13157,23 +12962,10 @@ var begin // Asynchronous timer for mousedown event on query tab close button TimerCloseTabByButton.Enabled := False; - - try - for i:=0 to QueryTabs.Count-1 do begin - if QueryTabs[i].CloseButton = FLastMouseDownCloseButton then begin - CloseQueryTab(QueryTabs[i].TabSheet.PageIndex); - break; - end; - end; - except - on E: Exception do - begin - // Stop timer on errors to prevent cascading issues - TimerCloseTabByButton.Enabled := False; - LogSQL(f_('Error in TimerCloseTabByButtonTimer: %s', [E.Message]), lcError); - - // Reset the close button reference to prevent further issues - FLastMouseDownCloseButton := nil; + for i:=0 to QueryTabs.Count-1 do begin + if QueryTabs[i].CloseButton = FLastMouseDownCloseButton then begin + CloseQueryTab(QueryTabs[i].TabSheet.PageIndex); + break; end; end; end; @@ -13528,13 +13320,6 @@ begin end; -procedure TMainForm.actProjectManagerExecute(Sender: TObject); -begin - // Toggle Project Manager tab visibility in current query tab - ToggleProjectManager; -end; - - procedure TMainForm.UpdateFilterPanel(Sender: TObject); var tab: TTabSheet; @@ -15292,68 +15077,11 @@ end; destructor TQueryTab.Destroy; begin - try - // Clean up Project Manager if it exists - if Assigned(projectManagerPanel) then - begin - try - projectManagerPanel.PrepareForShutdown; - except - // Ignore exceptions during shutdown preparation - end; - FreeAndNil(projectManagerPanel); - end; - - // Clean up tab components safely - if Assigned(ResultTabs) then - begin - ResultTabs.Clear; - FreeAndNil(ResultTabs); - end; - - // Free other components with error handling - try - if Assigned(DirectoryWatch) then - FreeAndNil(DirectoryWatch); - except - // Ignore DirectoryWatch cleanup errors - end; - - if Assigned(ListBindParams) then - FreeAndNil(ListBindParams); - - if Assigned(TimerLastChange) then - begin - TimerLastChange.Enabled := False; - FreeAndNil(TimerLastChange); - end; - - if Assigned(TimerStatusUpdate) then - begin - TimerStatusUpdate.Enabled := False; - FreeAndNil(TimerStatusUpdate); - end; - - except - on E: Exception do - begin - // Log but don't re-raise exceptions during destruction - {$IFDEF DEBUG} - OutputDebugString(PChar('Error in TQueryTab.Destroy: ' + E.Message)); - {$ENDIF} - end; - end; - - try - inherited Destroy; - except - on E: Exception do - begin - {$IFDEF DEBUG} - OutputDebugString(PChar('Error in TQueryTab.inherited Destroy: ' + E.Message)); - {$ENDIF} - end; - end; + ResultTabs.Clear; + DirectoryWatch.Free; + ListBindParams.Free; + TimerLastChange.Free; + TimerStatusUpdate.Free; end; @@ -16018,201 +15746,5 @@ begin Result := CompareText(Left.Name, Right.Name); end; - -{ TMainForm - Query Tab Backup Methods } - -function TMainForm.GetQueryTabBackupFilename(Tab: TQueryTab): String; -var - BackupDir, Timestamp, SafeCaption: String; -begin - Result := ''; - if not Assigned(Tab) then - Exit; - - try - // Create backup directory if it doesn't exist - if AppSettings.PortableMode then - BackupDir := ExtractFilePath(Application.ExeName) + 'Backups\' - else - BackupDir := AppSettings.DirnameUserAppData + 'Backups\'; - - if not DirectoryExists(BackupDir) then - ForceDirectories(BackupDir); - - // Create safe filename from tab caption - SafeCaption := Tab.TabSheet.Caption; - SafeCaption := StringReplace(SafeCaption, ' ', '_', [rfReplaceAll]); - SafeCaption := StringReplace(SafeCaption, '*', '', [rfReplaceAll]); - // Remove non-alphanumeric characters - SafeCaption := ReplaceRegExpr('[^\w]', SafeCaption, '_', True); - - // Generate unique filename with timestamp - Timestamp := FormatDateTime('yyyymmdd_hhnnss', Now); - Result := BackupDir + 'query_' + SafeCaption + '_' + Timestamp + '_' + IntToStr(GetTickCount64) + '.sql'; - except - on E: Exception do - begin - LogSQL(f_('Error generating backup filename: %s', [E.Message]), lcError); - Result := ''; - end; - end; -end; - -procedure TMainForm.SaveQueryTabBackupToIni(Tab: TQueryTab; const BackupFilename: String); -var - TabsIni: TIniFile; - TabSection: String; -begin - if not Assigned(Tab) or (BackupFilename = '') then - Exit; - - try - TabsIni := InitTabsIniFile; - try - TabSection := 'Tab' + IntToStr(Tab.Number); - TabsIni.WriteString(TabSection, 'BackupFile', BackupFilename); - TabsIni.WriteString(TabSection, 'BackupTimestamp', DateTimeToStr(Now)); - TabsIni.WriteString(TabSection, 'BackupReason', 'AutoBackupOnClose'); - finally - TabsIni.Free; - end; - except - on E: Exception do - begin - LogSQL(f_('Error saving backup info to tabs.ini: %s', [E.Message]), lcError); - end; - end; -end; - -function TMainForm.ShouldCreateQueryTabBackup(Tab: TQueryTab): Boolean; -var - QueryText: String; -begin - Result := False; - - if not Assigned(Tab) or not Assigned(Tab.Memo) then - Exit; - - // Only backup if content has been modified and is not empty - if not Tab.Memo.Modified then - Exit; - - QueryText := Trim(Tab.Memo.Text); - if Length(QueryText) = 0 then - Exit; - - // Don't backup very short queries (probably just typos) - if Length(QueryText) < 10 then - Exit; - - // Don't backup if it's just whitespace or common single commands - if (Pos(#13#10, QueryText) = 0) and - (LowerCase(QueryText) = 'select') or - (LowerCase(QueryText) = 'show') or - (LowerCase(QueryText) = 'desc') or - (LowerCase(QueryText) = 'describe') then - Exit; - - Result := True; -end; - -procedure TMainForm.ApplyProjectManagerStateToActiveTab; -var - CurrentTab: TQueryTab; -begin - // Apply global Project Manager state to the currently active query tab - if QueryTabs.HasActiveTab then - begin - CurrentTab := QueryTabs.ActiveTab; - if Assigned(CurrentTab.pcHelpers) then - begin - // Only change if different from current state to avoid unnecessary updates - if FProjectManagerTabActive and (CurrentTab.pcHelpers.ActivePage <> CurrentTab.tabProjectManager) then - begin - CurrentTab.pcHelpers.ActivePage := CurrentTab.tabProjectManager; - {$IFDEF DEBUG} - LogSQL('Applied Project Manager tab state to query tab', lcDebug); - {$ENDIF} - end - else if not FProjectManagerTabActive and (CurrentTab.pcHelpers.ActivePage <> CurrentTab.tabHelpers) then - begin - CurrentTab.pcHelpers.ActivePage := CurrentTab.tabHelpers; - {$IFDEF DEBUG} - LogSQL('Applied Helpers tab state to query tab', lcDebug); - {$ENDIF} - end; - end; - end; -end; - -procedure TMainForm.HelpersPageControlChange(Sender: TObject); -var - PageControl: TPageControl; - CurrentTab: TQueryTab; -begin - // Handle manual tab changes in helpers PageControl - PageControl := Sender as TPageControl; - - // Find which query tab this PageControl belongs to - CurrentTab := nil; - if QueryTabs.HasActiveTab then - CurrentTab := QueryTabs.ActiveTab; - - // Only update global state if this is the active query tab - if Assigned(CurrentTab) and (CurrentTab.pcHelpers = PageControl) then - begin - // Update global state based on which tab is now active - if PageControl.ActivePage = CurrentTab.tabProjectManager then - begin - if not FProjectManagerTabActive then - begin - FProjectManagerTabActive := True; - // Apply to all other query tabs - SynchronizeAllQueryTabsToGlobalState(CurrentTab); - // Update action state - if Assigned(actProjectManager) then - actProjectManager.Checked := True; - {$IFDEF DEBUG} - LogSQL('Project Manager manually activated - synchronized to all tabs', lcDebug); - {$ENDIF} - end; - end - else if PageControl.ActivePage = CurrentTab.tabHelpers then - begin - if FProjectManagerTabActive then - begin - FProjectManagerTabActive := False; - // Apply to all other query tabs - SynchronizeAllQueryTabsToGlobalState(CurrentTab); - // Update action state - if Assigned(actProjectManager) then - actProjectManager.Checked := False; - {$IFDEF DEBUG} - LogSQL('Helpers manually activated - synchronized to all tabs', lcDebug); - {$ENDIF} - end; - end; - end; -end; - -procedure TMainForm.SynchronizeAllQueryTabsToGlobalState(ExcludeTab: TQueryTab); -var - i: Integer; - Tab: TQueryTab; -begin - // Apply global state to all query tabs except the one that triggered the change - for i := 0 to QueryTabs.Count - 1 do - begin - Tab := QueryTabs[i]; - if (Tab <> ExcludeTab) and Assigned(Tab.pcHelpers) then - begin - if FProjectManagerTabActive then - Tab.pcHelpers.ActivePage := Tab.tabProjectManager - else - Tab.pcHelpers.ActivePage := Tab.tabHelpers; - end; - end; -end; - end. diff --git a/source/projectmanager.dfm b/source/projectmanager.dfm deleted file mode 100644 index 92a81e01..00000000 --- a/source/projectmanager.dfm +++ /dev/null @@ -1,152 +0,0 @@ -object ProjectManagerPanel: TProjectManagerPanel - Left = 0 - Top = 0 - Width = 250 - Height = 400 - Caption = 'Project Manager' - TabOrder = 0 - object pnlHeader: TPanel - Left = 0 - Top = 0 - Width = 250 - Height = 30 - Align = alTop - BevelOuter = bvNone - TabOrder = 0 - object lblProjectTitle: TLabel - Left = 8 - Top = 8 - Width = 43 - Height = 13 - Caption = 'Projects' - Font.Charset = DEFAULT_CHARSET - Font.Color = clWindowText - Font.Height = -11 - Font.Name = 'Tahoma' - Font.Style = [fsBold] - ParentFont = False - end - object ToolBarProjects: TToolBar - Left = 72 - Top = 0 - Width = 178 - Height = 30 - Align = alRight - AutoSize = True - ButtonHeight = 21 - ButtonWidth = 22 - Caption = 'ToolBarProjects' - Images = MainForm.VirtualImageListMain - ShowHint = True - TabOrder = 0 - object btnAddProject: TToolButton - Left = 0 - Top = 0 - Hint = 'Add project folder' - ImageIndex = 45 - ImageName = 'icons8-add' - OnClick = menuAddProjectClick - end - object btnRemoveProject: TToolButton - Left = 22 - Top = 0 - Hint = 'Remove project folder' - ImageIndex = 46 - ImageName = 'icons8-delete-button' - OnClick = menuRemoveProjectClick - end - object btnSeparator1: TToolButton - Left = 44 - Top = 0 - Width = 8 - Style = tbsSeparator - end - object btnRefreshProject: TToolButton - Left = 52 - Top = 0 - Hint = 'Refresh projects' - ImageIndex = 16 - ImageName = 'icons8-refresh' - OnClick = menuRefreshProjectClick - end - end - end - object pnlTree: TPanel - Left = 0 - Top = 30 - Width = 250 - Height = 370 - Align = alClient - BevelOuter = bvNone - TabOrder = 1 - object TreeProjects: TVirtualStringTree - Left = 0 - Top = 0 - Width = 250 - Height = 370 - Align = alClient - Header.AutoSizeIndex = 0 - Header.Options = [hoAutoResize, hoColumnResize, hoDrag, hoShowSortGlyphs, hoVisible] - Images = MainForm.VirtualImageListMain - PopupMenu = PopupProjects - TabOrder = 0 - TreeOptions.MiscOptions = [toAcceptOLEDrop, toFullRepaintOnResize, toInitOnSave, toToggleOnDblClick, toWheelPanning, toEditOnClick] - TreeOptions.PaintOptions = [toShowButtons, toShowDropmark, toShowTreeLines, toThemeAware, toUseBlendedImages, toShowRoot] - TreeOptions.SelectionOptions = [toFullRowSelect] - OnBeforeExpansion = TreeProjectsBeforeExpansion - OnCollapsed = TreeProjectsCollapsed - OnDblClick = TreeProjectsDblClick - OnExpanded = TreeProjectsExpanded - OnGetImageIndex = TreeProjectsGetImageIndex - OnGetNodeDataSize = TreeProjectsGetNodeDataSize - OnGetText = TreeProjectsGetText - OnInitChildren = TreeProjectsInitChildren - OnInitNode = TreeProjectsInitNode - Columns = < - item - Position = 0 - Text = 'Name' - Width = 246 - end> - end - end - object PopupProjects: TPopupMenu - OnPopup = PopupProjectsPopup - Left = 104 - Top = 168 - object menuAddProject: TMenuItem - Caption = 'Add Project Folder...' - ImageIndex = 45 - ImageName = 'icons8-add' - OnClick = menuAddProjectClick - end - object menuRemoveProject: TMenuItem - Caption = 'Remove Project' - ImageIndex = 46 - ImageName = 'icons8-delete-button' - OnClick = menuRemoveProjectClick - end - object menuSeparator1: TMenuItem - Caption = '-' - end - object menuOpenFile: TMenuItem - Caption = 'Open File' - Default = True - ImageIndex = 8 - ImageName = 'icons8-opened-folder' - OnClick = menuOpenFileClick - end - object menuOpenInExplorer: TMenuItem - Caption = 'Open in Explorer' - ImageIndex = 71 - ImageName = 'icons8-folder' - OnClick = menuOpenInExplorerClick - end - object menuRefreshProject: TMenuItem - Caption = 'Refresh' - ImageIndex = 16 - ImageName = 'icons8-refresh' - OnClick = menuRefreshProjectClick - end - end -end diff --git a/source/projectmanager.pas b/source/projectmanager.pas deleted file mode 100644 index ab5e9117..00000000 --- a/source/projectmanager.pas +++ /dev/null @@ -1,1715 +0,0 @@ -unit projectmanager; - -interface - -uses - System.Classes, System.SysUtils, System.Math, Vcl.Controls, Vcl.Forms, Vcl.ExtCtrls, - Vcl.ComCtrls, Vcl.ToolWin, Vcl.StdCtrls, Vcl.Menus, Vcl.Graphics, - Vcl.Dialogs, Vcl.Clipbrd, System.IOUtils, Vcl.FileCtrl, ShellAPI, Windows, System.IniFiles, - System.Generics.Collections, VirtualTrees, VirtualTrees.Types, VirtualTrees.BaseTree, - apphelpers, System.UITypes, gnugettext, ComObj, ShlObj, ActiveX; - -type - // Project folder information - TProjectFolder = class - public - Name: string; - Path: string; - constructor Create(const AName, APath: string); - end; - - // Node data for VirtualTree - PProjectNodeData = ^TProjectNodeData; - TProjectNodeData = record - NodeType: (ntProject, ntFolder, ntFile); - ProjectIndex: Integer; // Only for ntProject - index into FProjectFolders - FullPath: string; // For ntFolder and ntFile - DisplayName: string; // Display text - end; - -const - // UI Layout Constants - HEADER_PANEL_HEIGHT = 26; - TOOLBAR_HEIGHT = 28; - TOOLBAR_BUTTON_HEIGHT = 21; - TOOLBAR_BUTTON_WIDTH = 22; - DEFAULT_PANEL_WIDTH = 300; - DEFAULT_PANEL_HEIGHT = 200; - DEFAULT_NODE_HEIGHT = 20; - HEADER_LABEL_LEFT = 8; - HEADER_LABEL_TOP = 6; - - // File handling - MAX_FILES_PER_FOLDER = 100; - - // System icons (using HeidiSQL's VirtualImageListMain indices) - ICON_ADD_PROJECT = 45; // Same as actDataInsert - ICON_REMOVE_PROJECT = 46; // Same as actDataDelete - -type - // Project Manager Panel - rechts positioniert wie andere Tool-Panels - TProjectManagerPanel = class(TPanel) - private - FToolBar: TToolBar; - FTreeView: TVirtualStringTree; - FHeaderPanel: TPanel; - FHeaderLabel: TLabel; - FBtnAddProject: TToolButton; - FBtnRemoveProject: TToolButton; - FBtnRenameProject: TToolButton; - FBtnRefresh: TToolButton; - FPopupMenu: TPopupMenu; - FControlsCreated: Boolean; - FProjectFolders: TObjectList; - - procedure CreateControls; - procedure SetupToolbar; - procedure SetupTreeView; - procedure SetupPopupMenu; - procedure BtnAddProjectClick(Sender: TObject); - procedure BtnRemoveProjectClick(Sender: TObject); - procedure BtnRenameProjectClick(Sender: TObject); - procedure BtnRefreshClick(Sender: TObject); - procedure TreeViewGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string); - procedure TreeViewGetHint(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var LineBreakStyle: TVTTooltipLineBreakStyle; var HintText: string); - procedure TreeViewInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); - procedure TreeViewInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal); - procedure TreeViewGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: TImageIndex); - procedure TreeViewFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode); - procedure TreeViewDblClick(Sender: TObject); - procedure TreeViewKeyPress(Sender: TObject; var Key: Char); - procedure TreeViewPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType); - procedure PopupOpenFileClick(Sender: TObject); - procedure PopupOpenWithSystemEditorClick(Sender: TObject); - procedure PopupOpenFolderClick(Sender: TObject); - procedure PopupCopyPathClick(Sender: TObject); - procedure PopupRemoveProjectClick(Sender: TObject); - procedure PopupRenameProjectClick(Sender: TObject); - - procedure LoadProjects; - procedure SaveProjects; - procedure RefreshProjectTree; - function InitProjectsIniFile: TIniFile; - function IsTextFile(const FileName: string): Boolean; - function IsFileOpenInTab(const FilePath: string): Boolean; - procedure OpenFileInQueryTab(const FilePath: string); - procedure UpdateTreeAppearance; // Update visual state without rebuilding tree - function IsShuttingDown: Boolean; // Check if component is being destroyed - protected - procedure CreateWnd; override; - public - constructor Create(AOwner: TComponent); override; - destructor Destroy; override; - procedure PrepareForShutdown; // Call this before application shutdown - procedure ToggleVisibility; // Toggle panel visibility - end; - -implementation - -uses main; - -// Modern Windows folder selection dialog interfaces and constants -const - FOS_PICKFOLDERS = $00000020; - FOS_FORCEFILESYSTEM = $00000040; - FOS_PATHMUSTEXIST = $00000800; - SIGDN_FILESYSPATH = $80058000; - -type - IShellItem = interface(IUnknown) - ['{43826d1e-e718-42ee-bc55-a1e261c37bfe}'] - function BindToHandler(const pbc: IBindCtx; const bhid: TGUID; const riid: TGUID; out ppv): HRESULT; stdcall; - function GetParent(out ppsi: IShellItem): HRESULT; stdcall; - function GetDisplayName(sigdnName: DWORD; out ppszName: PWideChar): HRESULT; stdcall; - function GetAttributes(sfgaoMask: DWORD; out psfgaoAttribs: DWORD): HRESULT; stdcall; - function Compare(const psi: IShellItem; hint: DWORD; out piOrder: Integer): HRESULT; stdcall; - end; - - IFileDialog = interface(IModalWindow) - ['{42f85136-db7e-439c-85f1-e4075d135fc8}'] - function SetFileTypes(cFileTypes: UINT; const rgFilterSpec): HRESULT; stdcall; - function SetFileTypeIndex(iFileType: UINT): HRESULT; stdcall; - function GetFileTypeIndex(out piFileType: UINT): HRESULT; stdcall; - function Advise(const pfde: IUnknown; out pdwCookie: DWORD): HRESULT; stdcall; - function Unadvise(dwCookie: DWORD): HRESULT; stdcall; - function SetOptions(fos: DWORD): HRESULT; stdcall; - function GetOptions(out pfos: DWORD): HRESULT; stdcall; - function SetDefaultFolder(const psi: IShellItem): HRESULT; stdcall; - function SetFolder(const psi: IShellItem): HRESULT; stdcall; - function GetFolder(out ppsi: IShellItem): HRESULT; stdcall; - function GetCurrentSelection(out ppsi: IShellItem): HRESULT; stdcall; - function SetFileName(pszName: PWideChar): HRESULT; stdcall; - function GetFileName(out pszName: PWideChar): HRESULT; stdcall; - function SetTitle(pszTitle: PWideChar): HRESULT; stdcall; - function SetOkButtonLabel(pszText: PWideChar): HRESULT; stdcall; - function SetFileNameLabel(pszLabel: PWideChar): HRESULT; stdcall; - function GetResult(out ppsi: IShellItem): HRESULT; stdcall; - function AddPlace(const psi: IShellItem; fdap: DWORD): HRESULT; stdcall; - function SetDefaultExtension(pszDefaultExtension: PWideChar): HRESULT; stdcall; - function Close(hr: HRESULT): HRESULT; stdcall; - function SetClientGuid(const guid: TGUID): HRESULT; stdcall; - function ClearClientData: HRESULT; stdcall; - function SetFilter(const pFilter: IUnknown): HRESULT; stdcall; - end; - - IFileOpenDialog = interface(IFileDialog) - ['{d57c7288-d4ad-4768-be02-9d969532d960}'] - function GetResults(out ppenum: IUnknown): HRESULT; stdcall; - function GetSelectedItems(out ppsai: IUnknown): HRESULT; stdcall; - end; - - IModalWindow = interface(IUnknown) - ['{b4db1657-70d7-485e-8e3e-6fcb5a5c1802}'] - function Show(hwndOwner: HWND): HRESULT; stdcall; - end; - -const - CLSID_FileOpenDialog: TGUID = '{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}'; - -{ TProjectFolder } - -constructor TProjectFolder.Create(const AName, APath: string); -begin - inherited Create; - Name := AName; - Path := APath; -end; - -{ TProjectManagerPanel } - -constructor TProjectManagerPanel.Create(AOwner: TComponent); -begin - inherited Create(AOwner); - - // Basic panel setup - positioned in right panel at bottom - Align := alBottom; // Position at bottom of right panel - Caption := ''; // No caption to avoid visual clutter - BevelOuter := bvNone; // Remove border to match other panels - BevelInner := bvNone; // Remove inner border - BorderStyle := bsNone; // Remove border completely - Color := GetThemeColor(clWindow); // Use theme-appropriate background - Width := DEFAULT_PANEL_WIDTH; // Use constant - Height := DEFAULT_PANEL_HEIGHT; // Use constant - Visible := True; // Default visible - - // Initialize project folders list - FProjectFolders := TObjectList.Create(True); // True = owns objects - - // Components will be created in CreateWnd when the handle is available - FControlsCreated := False; -end; - -procedure TProjectManagerPanel.CreateWnd; -begin - inherited CreateWnd; - - // Create sub-components only after handle creation - if not FControlsCreated then begin - try - CreateControls; - LoadProjects; - RefreshProjectTree; - FControlsCreated := True; - except - on E: Exception do - begin - ErrorDialog(_('Error in CreateWnd: ') + E.Message); - end; - end; - end; -end; - -destructor TProjectManagerPanel.Destroy; -begin - try - // Save projects before cleanup - if Assigned(FProjectFolders) then - SaveProjects; - - // Clear tree view safely before destroying - if Assigned(FTreeView) then - begin - // First, disconnect all event handlers to prevent callbacks during destruction - FTreeView.OnGetText := nil; - FTreeView.OnGetHint := nil; - FTreeView.OnInitNode := nil; - FTreeView.OnInitChildren := nil; - FTreeView.OnGetImageIndex := nil; - FTreeView.OnFreeNode := nil; - FTreeView.OnDblClick := nil; - FTreeView.OnKeyPress := nil; - FTreeView.OnPaintText := nil; - - // Clear the tree content before destroying - FTreeView.Clear; - - // Ensure parent relationship is cleared - if Assigned(FTreeView.Parent) then - FTreeView.Parent := nil; - end; - - // Clean up project folders - TObjectList will free objects automatically - FreeAndNil(FProjectFolders); - - // Disconnect from parent to avoid double-free issues - if Assigned(Parent) then - Parent := nil; - - except - on E: Exception do - begin - // Suppress any errors during destruction to prevent access violations - // but in debug mode, we might want to know about them - {$IFDEF DEBUG} - OutputDebugString(PChar('Error in TProjectManagerPanel.Destroy: ' + E.Message)); - {$ENDIF} - end; - end; - - try - inherited Destroy; - except - on E: Exception do - begin - // Final safety net - log but don't re-raise - {$IFDEF DEBUG} - OutputDebugString(PChar('Error in inherited Destroy: ' + E.Message)); - {$ENDIF} - end; - end; -end; - -procedure TProjectManagerPanel.PrepareForShutdown; -begin - try - // Save projects before shutdown - if Assigned(FProjectFolders) then - SaveProjects; - - // Clear event handlers to prevent access violations during shutdown - if Assigned(FTreeView) then - begin - FTreeView.OnGetText := nil; - FTreeView.OnGetHint := nil; - FTreeView.OnInitNode := nil; - FTreeView.OnInitChildren := nil; - FTreeView.OnGetImageIndex := nil; - FTreeView.OnFreeNode := nil; - FTreeView.OnDblClick := nil; - FTreeView.OnKeyPress := nil; - FTreeView.OnPaintText := nil; - FTreeView.Clear; - end; - except - // Suppress any errors during shutdown preparation - end; -end; - -procedure TProjectManagerPanel.CreateControls; -begin - // Header Panel with title - FHeaderPanel := TPanel.Create(Self); - FHeaderPanel.Parent := Self; - FHeaderPanel.Align := alTop; - FHeaderPanel.Height := HEADER_PANEL_HEIGHT; - FHeaderPanel.BevelOuter := bvNone; - FHeaderPanel.BevelInner := bvNone; - FHeaderPanel.BorderStyle := bsNone; - FHeaderPanel.Color := GetThemeColor(clWindow); - FHeaderPanel.Caption := ''; - - FHeaderLabel := TLabel.Create(Self); - FHeaderLabel.Parent := FHeaderPanel; - FHeaderLabel.Left := HEADER_LABEL_LEFT; - FHeaderLabel.Top := HEADER_LABEL_TOP; - FHeaderLabel.Caption := _('Project Manager'); - FHeaderLabel.Font.Style := [fsBold]; - FHeaderLabel.Font.Size := 9; - FHeaderLabel.Font.Color := GetThemeColor(clWindowText); - - SetupToolbar; - SetupPopupMenu; - SetupTreeView; -end; - -procedure TProjectManagerPanel.SetupToolbar; -begin - FToolBar := TToolBar.Create(Self); - FToolBar.Parent := Self; - FToolBar.Align := alTop; - FToolBar.Height := 28; - FToolBar.ShowCaptions := False; // Icons only, no captions - FToolBar.ButtonHeight := 21; - FToolBar.ButtonWidth := 22; - FToolBar.Flat := True; - FToolBar.Transparent := False; - FToolBar.Color := GetThemeColor(clWindow); - FToolBar.AutoSize := True; - FToolBar.Images := MainForm.VirtualImageListMain; // Use HeidiSQL's main image list - FToolBar.ShowHint := True; - FToolBar.BorderWidth := 0; - FToolBar.EdgeBorders := []; - FToolBar.EdgeInner := esNone; - FToolBar.EdgeOuter := esNone; - - // Add Project Button - use same ImageIndex as actDataInsert (icons8-add) - FBtnAddProject := TToolButton.Create(FToolBar); - FBtnAddProject.Parent := FToolBar; - FBtnAddProject.ImageIndex := 45; // Same as actDataInsert - icons8-add - FBtnAddProject.Hint := _('Add Project Folder'); - FBtnAddProject.ShowHint := True; - FBtnAddProject.OnClick := BtnAddProjectClick; - - // Remove Project Button - use same ImageIndex as actDataDelete (icons8-delete-button) - FBtnRemoveProject := TToolButton.Create(FToolBar); - FBtnRemoveProject.Parent := FToolBar; - FBtnRemoveProject.ImageIndex := 46; // Same as actDataDelete - icons8-delete-button - FBtnRemoveProject.Hint := _('Remove Project Folder'); - FBtnRemoveProject.ShowHint := True; - FBtnRemoveProject.OnClick := BtnRemoveProjectClick; - - // Rename Project Button - use same ImageIndex as actRenameQueryTab - FBtnRenameProject := TToolButton.Create(FToolBar); - FBtnRenameProject.Parent := FToolBar; - FBtnRenameProject.ImageIndex := MainForm.actRenameQueryTab.ImageIndex; - FBtnRenameProject.Hint := _('Rename Project'); - FBtnRenameProject.ShowHint := True; - FBtnRenameProject.OnClick := BtnRenameProjectClick; - - // Separator - with TToolButton.Create(FToolBar) do begin - Parent := FToolBar; - Style := tbsSeparator; - Width := 8; - end; - - // Refresh Button - use same ImageIndex as actRefresh (icons8-circular-arrow-100) - FBtnRefresh := TToolButton.Create(FToolBar); - FBtnRefresh.Parent := FToolBar; - FBtnRefresh.ImageIndex := MainForm.actRefresh.ImageIndex; // Use the actual refresh action ImageIndex - FBtnRefresh.Hint := _('Refresh Projects'); - FBtnRefresh.ShowHint := True; - FBtnRefresh.OnClick := BtnRefreshClick; -end; - -procedure TProjectManagerPanel.SetupTreeView; -begin - FTreeView := TVirtualStringTree.Create(Self); - FTreeView.Parent := Self; - FTreeView.Align := alClient; - FTreeView.BorderStyle := bsNone; // Remove border to match other panels - FTreeView.Color := GetThemeColor(clWindow); - FTreeView.Colors.BorderColor := GetThemeColor(clWindow); // No border color - FTreeView.Colors.DisabledColor := GetThemeColor(clGrayText); - FTreeView.Colors.DropMarkColor := GetThemeColor(clHighlight); - FTreeView.Colors.DropTargetColor := GetThemeColor(clHighlight); - FTreeView.Colors.FocusedSelectionColor := GetThemeColor(clHighlight); - FTreeView.Colors.FocusedSelectionBorderColor := GetThemeColor(clHighlight); - FTreeView.Colors.GridLineColor := GetThemeColor(clBtnShadow); - FTreeView.Colors.HeaderHotColor := GetThemeColor(clBtnFace); - FTreeView.Colors.HotColor := GetThemeColor(clBtnFace); - FTreeView.Colors.SelectionRectangleBlendColor := GetThemeColor(clHighlight); - FTreeView.Colors.SelectionRectangleBorderColor := GetThemeColor(clHighlight); - FTreeView.Colors.TreeLineColor := GetThemeColor(clBtnShadow); - FTreeView.Colors.UnfocusedSelectionColor := GetThemeColor(clBtnFace); - FTreeView.Colors.UnfocusedSelectionBorderColor := GetThemeColor(clBtnFace); - FTreeView.DefaultNodeHeight := 20; // Normal height since we're not using two lines - FTreeView.Font.Size := 9; - FTreeView.Font.Color := GetThemeColor(clWindowText); - FTreeView.Header.AutoSizeIndex := 0; - FTreeView.Header.DefaultHeight := 17; - FTreeView.Header.Height := 17; - FTreeView.Header.MainColumn := -1; - FTreeView.Header.Options := []; - FTreeView.HintMode := hmHint; - FTreeView.HotCursor := crHandPoint; - FTreeView.Images := GetSystemImageList; - FTreeView.IncrementalSearch := isAll; - FTreeView.NodeDataSize := SizeOf(TProjectNodeData); - FTreeView.ParentShowHint := False; - FTreeView.SelectionCurveRadius := 0; - FTreeView.ShowHint := True; - FTreeView.TabOrder := 0; - FTreeView.TreeOptions.AutoOptions := [toAutoDropExpand, toAutoScrollOnExpand, toAutoTristateTracking, toAutoDeleteMovedNodes]; - FTreeView.TreeOptions.MiscOptions := [toAcceptOLEDrop, toFullRepaintOnResize, toInitOnSave, toToggleOnDblClick, toWheelPanning]; - FTreeView.TreeOptions.PaintOptions := [toShowButtons, toShowDropmark, toShowTreeLines, toThemeAware]; - FTreeView.TreeOptions.SelectionOptions := [toFullRowSelect]; - - // Event handlers - FTreeView.OnGetText := TreeViewGetText; - FTreeView.OnGetHint := TreeViewGetHint; - FTreeView.OnInitNode := TreeViewInitNode; - FTreeView.OnInitChildren := TreeViewInitChildren; - FTreeView.OnGetImageIndex := TreeViewGetImageIndex; - FTreeView.OnFreeNode := TreeViewFreeNode; - FTreeView.OnDblClick := TreeViewDblClick; - FTreeView.OnKeyPress := TreeViewKeyPress; - FTreeView.OnPaintText := TreeViewPaintText; - - // Set PopupMenu after all event handlers are assigned - FTreeView.PopupMenu := FPopupMenu; -end; - -procedure TProjectManagerPanel.SetupPopupMenu; -var - MenuItem: TMenuItem; -begin - FPopupMenu := TPopupMenu.Create(Self); - FPopupMenu.Images := MainForm.VirtualImageListMain; // Use HeidiSQL's main image list - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := _('Open File'); - MenuItem.ImageIndex := MainForm.actLoadSQL.ImageIndex; // Use same as actLoadSQL (icons8-folder) - MenuItem.OnClick := PopupOpenFileClick; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := _('Open with System Editor'); - MenuItem.ImageIndex := MainForm.actLoadSQL.ImageIndex; // Use same as actLoadSQL (icons8-folder) - MenuItem.OnClick := PopupOpenWithSystemEditorClick; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := _('Open Folder'); - MenuItem.ImageIndex := MainForm.actLoadSQL.ImageIndex; // Use same as actLoadSQL (icons8-folder) - MenuItem.OnClick := PopupOpenFolderClick; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := '-'; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := _('Copy Path'); - MenuItem.ImageIndex := MainForm.actCopy.ImageIndex; // Use copy action - MenuItem.OnClick := PopupCopyPathClick; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := '-'; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := _('Add Project Folder...'); - MenuItem.ImageIndex := 45; // Same as actDataInsert - icons8-add - MenuItem.OnClick := BtnAddProjectClick; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := _('Remove Project'); - MenuItem.ImageIndex := 46; // Same as actDataDelete - icons8-delete-button - MenuItem.OnClick := PopupRemoveProjectClick; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := _('Rename Project'); - MenuItem.ImageIndex := MainForm.actRenameQueryTab.ImageIndex; // Use rename query tab action ImageIndex - MenuItem.OnClick := PopupRenameProjectClick; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := '-'; - FPopupMenu.Items.Add(MenuItem); - - MenuItem := TMenuItem.Create(FPopupMenu); - MenuItem.Caption := _('Refresh'); - MenuItem.ImageIndex := MainForm.actRefresh.ImageIndex; // Use actual refresh action ImageIndex - MenuItem.OnClick := BtnRefreshClick; - FPopupMenu.Items.Add(MenuItem); -end; - -procedure TProjectManagerPanel.BtnAddProjectClick(Sender: TObject); -var - FolderPath, ProjectName: string; - ProjectFolder: TProjectFolder; - i: Integer; - FileOpenDialog: IFileOpenDialog; - hr: HRESULT; - ShellItem: IShellItem; - FolderPathPWideChar: PWideChar; -begin - // Use modern Windows folder selection dialog - FolderPath := ''; - - hr := CoCreateInstance(CLSID_FileOpenDialog, nil, CLSCTX_INPROC_SERVER, IFileOpenDialog, FileOpenDialog); - if SUCCEEDED(hr) then - begin - try - // Configure the dialog for folder selection - FileOpenDialog.SetOptions(FOS_PICKFOLDERS or FOS_FORCEFILESYSTEM or FOS_PATHMUSTEXIST); - FileOpenDialog.SetTitle(PWideChar(_('Select Project Folder'))); - - // Show the dialog - hr := FileOpenDialog.Show(Application.MainForm.Handle); - if SUCCEEDED(hr) then - begin - hr := FileOpenDialog.GetResult(ShellItem); - if SUCCEEDED(hr) then - begin - hr := ShellItem.GetDisplayName(SIGDN_FILESYSPATH, FolderPathPWideChar); - if SUCCEEDED(hr) then - begin - FolderPath := FolderPathPWideChar; - CoTaskMemFree(FolderPathPWideChar); - end; - end; - end; - except - // If modern dialog fails, fall back to old dialog - SelectDirectory(_('Select Project Folder'), '', FolderPath); - end; - end - else - begin - // Fall back to old dialog if COM interface creation fails - if not SelectDirectory(_('Select Project Folder'), '', FolderPath) then - Exit; - end; - - // Continue with existing logic if a folder was selected - if FolderPath <> '' then - begin - ProjectName := ExtractFileName(FolderPath); - if ProjectName = '' then - ProjectName := _('Root'); - - // Allow user to customize the project name - if InputQuery(_('Project Name'), _('Enter a name for this project:'), ProjectName) then - begin - // Trim whitespace and check for empty name - ProjectName := Trim(ProjectName); - if ProjectName = '' then - begin - ErrorDialog(_('Project name cannot be empty!')); - Exit; - end; - - // Check if project name already exists - for i := 0 to FProjectFolders.Count - 1 do - begin - if SameText(FProjectFolders[i].Name, ProjectName) then - begin - ErrorDialog(_('A project with this name already exists!')); - Exit; - end; - end; - - // Check if project path already exists - for i := 0 to FProjectFolders.Count - 1 do - begin - if SameText(FProjectFolders[i].Path, FolderPath) then - begin - ErrorDialog(_('This folder is already added as a project!')); - Exit; - end; - end; - - // Add new project - ProjectFolder := TProjectFolder.Create(ProjectName, FolderPath); - FProjectFolders.Add(ProjectFolder); - - SaveProjects; - - // Clear selection before refreshing to avoid issues - FTreeView.ClearSelection; - RefreshProjectTree; - end; - end; -end; - -procedure TProjectManagerPanel.BtnRemoveProjectClick(Sender: TObject); -var - Node: PVirtualNode; - Data: PProjectNodeData; - ProjectIndex: Integer; - ProjectName: string; -begin - Node := FTreeView.FocusedNode; - if not Assigned(Node) then - begin - ErrorDialog(_('Please select a project to remove.')); - Exit; - end; - - Data := FTreeView.GetNodeData(Node); - if not Assigned(Data) then Exit; - - // Find root project node and store project index and name - ProjectName := ''; - - while Assigned(Node.Parent) do - begin - Node := Node.Parent; - Data := FTreeView.GetNodeData(Node); - end; - - if Assigned(Data) and (Data^.NodeType = ntProject) then - begin - // Use the project index for safety - ProjectIndex := Data^.ProjectIndex; - - // Validate the project index - if (ProjectIndex < 0) or (ProjectIndex >= FProjectFolders.Count) then - begin - ErrorDialog(_('Project not found in list.')); - Exit; - end; - - // Store the project name - ProjectName := FProjectFolders[ProjectIndex].Name; - - if MessageDialog(f_('Remove project "%s"?', [ProjectName]), - mtConfirmation, [mbYes, mbNo]) = mrYes then - begin - // Remove using the stored index - FProjectFolders.Delete(ProjectIndex); - - SaveProjects; - - // Clear selection before refreshing to avoid issues - FTreeView.ClearSelection; - RefreshProjectTree; - end; - end; -end; - -procedure TProjectManagerPanel.BtnRenameProjectClick(Sender: TObject); -var - Node: PVirtualNode; - Data: PProjectNodeData; - NewName: string; - i: Integer; - ProjectIndex: Integer; - ProjectName: string; -begin - Node := FTreeView.FocusedNode; - if not Assigned(Node) then - begin - ErrorDialog(_('Please select a project to rename.')); - Exit; - end; - - Data := FTreeView.GetNodeData(Node); - if not Assigned(Data) then Exit; - - // Find root project node and store project index and name - ProjectName := ''; - - while Assigned(Node.Parent) do - begin - Node := Node.Parent; - Data := FTreeView.GetNodeData(Node); - end; - - if Assigned(Data) and (Data^.NodeType = ntProject) then - begin - // Use the project index for safety - ProjectIndex := Data^.ProjectIndex; - - // Validate the project index - if (ProjectIndex < 0) or (ProjectIndex >= FProjectFolders.Count) then - begin - ErrorDialog(_('Project not found in list.')); - Exit; - end; - - // Store the project name - ProjectName := FProjectFolders[ProjectIndex].Name; - - NewName := ProjectName; - - if InputQuery(_('Rename Project'), _('Enter new name for project:'), NewName) then - begin - // Trim whitespace and check for empty name - NewName := Trim(NewName); - if NewName = '' then - begin - ErrorDialog(_('Project name cannot be empty!')); - Exit; - end; - - // Check if the new name is different from the current name - if SameText(NewName, ProjectName) then - begin - // Name unchanged, nothing to do - Exit; - end; - - // Check if project name already exists - for i := 0 to FProjectFolders.Count - 1 do - begin - if (i <> ProjectIndex) and SameText(FProjectFolders[i].Name, NewName) then - begin - ErrorDialog(_('A project with this name already exists!')); - Exit; - end; - end; - - // Update the project name using the stored index - FProjectFolders[ProjectIndex].Name := NewName; - - SaveProjects; - - // Clear selection before refreshing to avoid issues - FTreeView.ClearSelection; - RefreshProjectTree; - end; - end - else - begin - ErrorDialog(_('Please select a project to rename.')); - end; -end; - -procedure TProjectManagerPanel.BtnRefreshClick(Sender: TObject); -begin - RefreshProjectTree; -end; - -procedure TProjectManagerPanel.TreeViewGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string); -var - Data: PProjectNodeData; -begin - // Safety check for shutdown - if IsShuttingDown then - begin - CellText := ''; - Exit; - end; - - Data := Sender.GetNodeData(Node); - if Assigned(Data) then - begin - case Data^.NodeType of - ntProject: - if (Data^.ProjectIndex >= 0) and (Data^.ProjectIndex < FProjectFolders.Count) then - begin - if System.SysUtils.DirectoryExists(FProjectFolders[Data^.ProjectIndex].Path) then - CellText := FProjectFolders[Data^.ProjectIndex].Name - else - CellText := FProjectFolders[Data^.ProjectIndex].Name + ' ' + _('(Path not found)'); - end - else - CellText := _('(Invalid Project)'); - ntFolder: CellText := Data^.DisplayName; - ntFile: CellText := Data^.DisplayName; - end; - end - else if FProjectFolders.Count = 0 then - begin - // Show placeholder text for empty state - if Node.Index = 0 then - CellText := _('No projects configured') - else - CellText := _('Click Add to add a project folder'); - end; -end; - -procedure TProjectManagerPanel.TreeViewGetHint(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var LineBreakStyle: TVTTooltipLineBreakStyle; var HintText: string); -var - Data: PProjectNodeData; -begin - Data := Sender.GetNodeData(Node); - if Assigned(Data) then - begin - case Data^.NodeType of - ntProject: - if (Data^.ProjectIndex >= 0) and (Data^.ProjectIndex < FProjectFolders.Count) then - begin - if System.SysUtils.DirectoryExists(FProjectFolders[Data^.ProjectIndex].Path) then - HintText := FProjectFolders[Data^.ProjectIndex].Path - else - HintText := FProjectFolders[Data^.ProjectIndex].Path + ' ' + _('(Path not found)'); - end - else - HintText := _('(Invalid Project)'); - ntFolder: HintText := Data^.FullPath; - ntFile: HintText := Data^.FullPath; - end; - end; -end; - -procedure TProjectManagerPanel.TreeViewInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); -var - Data: PProjectNodeData; - ParentData: PProjectNodeData; - SearchRec: TSearchRec; - FolderPath: string; - CurrentIndex: Integer; - FileCount: Integer; - FindResult: Integer; -begin - Data := Sender.GetNodeData(Node); - if not Assigned(Data) then Exit; - - if not Assigned(ParentNode) then - begin - // Root level - project folders - if (Integer(Node.Index) < FProjectFolders.Count) then - begin - Data^.NodeType := ntProject; - Data^.ProjectIndex := Node.Index; - Data^.FullPath := FProjectFolders[Node.Index].Path; - Data^.DisplayName := FProjectFolders[Node.Index].Name; - - // Check if project folder has children - if System.SysUtils.DirectoryExists(FProjectFolders[Node.Index].Path) then - Include(InitialStates, ivsHasChildren); - end; - end - else - begin - // Child nodes - need to determine what this node represents - ParentData := Sender.GetNodeData(ParentNode); - if not Assigned(ParentData) then Exit; - - // Determine parent folder path - case ParentData^.NodeType of - ntProject: - if (ParentData^.ProjectIndex >= 0) and (ParentData^.ProjectIndex < FProjectFolders.Count) then - FolderPath := FProjectFolders[ParentData^.ProjectIndex].Path - else - Exit; - ntFolder: FolderPath := ParentData^.FullPath; - else Exit; - end; - - if not System.SysUtils.DirectoryExists(FolderPath) then Exit; - - CurrentIndex := 0; - - // First pass: add directories with error handling - try - FindResult := FindFirst(IncludeTrailingPathDelimiter(FolderPath) + '*', faDirectory, SearchRec); - if FindResult = 0 then - begin - try - repeat - if (SearchRec.Attr and faDirectory) <> 0 then - begin - if (SearchRec.Name <> '.') and (SearchRec.Name <> '..') then - begin - if CurrentIndex = Integer(Node.Index) then - begin - Data^.NodeType := ntFolder; - Data^.DisplayName := SearchRec.Name; - Data^.FullPath := IncludeTrailingPathDelimiter(FolderPath) + SearchRec.Name; - - // Check if this folder has children - if System.SysUtils.DirectoryExists(Data^.FullPath) then - Include(InitialStates, ivsHasChildren); - - Exit; - end; - Inc(CurrentIndex); - end; - end; - until FindNext(SearchRec) <> 0; - finally - System.SysUtils.FindClose(SearchRec); - end; - end; - except - // Ignore file system errors during directory enumeration - end; - - // Second pass: add files with error handling - FileCount := 0; - try - FindResult := FindFirst(IncludeTrailingPathDelimiter(FolderPath) + '*', faAnyFile, SearchRec); - if FindResult = 0 then - begin - try - repeat - if (SearchRec.Attr and faDirectory) = 0 then - begin - if IsTextFile(SearchRec.Name) then - begin - if CurrentIndex = Integer(Node.Index) then - begin - if FileCount >= 100 then - begin - Data^.NodeType := ntFile; - Data^.DisplayName := _('... (more files)'); - Data^.FullPath := ''; - end - else - begin - Data^.NodeType := ntFile; - Data^.DisplayName := SearchRec.Name; - Data^.FullPath := IncludeTrailingPathDelimiter(FolderPath) + SearchRec.Name; - end; - Exit; - end; - Inc(CurrentIndex); - Inc(FileCount); - if FileCount >= 100 then - begin - if CurrentIndex = Integer(Node.Index) then - begin - Data^.NodeType := ntFile; - Data^.DisplayName := _('... (more files)'); - Data^.FullPath := ''; - Exit; - end; - Inc(CurrentIndex); - Break; - end; - end; - end; - until FindNext(SearchRec) <> 0; - finally - System.SysUtils.FindClose(SearchRec); - end; - end; - except - // Ignore file system errors during file enumeration - end; - end; -end; - -procedure TProjectManagerPanel.TreeViewInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal); -var - Data: PProjectNodeData; - SearchRec: TSearchRec; - FolderPath: string; - FileCount, DirCount: Integer; - FindResult: Integer; -begin - ChildCount := 0; - Data := Sender.GetNodeData(Node); - - if not Assigned(Data) then Exit; - - // Determine folder path - case Data^.NodeType of - ntProject: - if (Data^.ProjectIndex >= 0) and (Data^.ProjectIndex < FProjectFolders.Count) then - FolderPath := FProjectFolders[Data^.ProjectIndex].Path - else - Exit; - ntFolder: FolderPath := Data^.FullPath; - else Exit; - end; - - if not System.SysUtils.DirectoryExists(FolderPath) then Exit; - - FileCount := 0; - DirCount := 0; - - // Count directories with error handling - try - FindResult := FindFirst(IncludeTrailingPathDelimiter(FolderPath) + '*', faDirectory, SearchRec); - if FindResult = 0 then - begin - try - repeat - if (SearchRec.Attr and faDirectory) <> 0 then - begin - if (SearchRec.Name <> '.') and (SearchRec.Name <> '..') then - Inc(DirCount); - end; - until FindNext(SearchRec) <> 0; - finally - System.SysUtils.FindClose(SearchRec); - end; - end; - except - // Ignore file system errors during directory enumeration - DirCount := 0; - end; - - // Count text files (limit to 100) with error handling - try - FindResult := FindFirst(IncludeTrailingPathDelimiter(FolderPath) + '*', faAnyFile, SearchRec); - if FindResult = 0 then - begin - try - repeat - if (SearchRec.Attr and faDirectory) = 0 then - begin - if IsTextFile(SearchRec.Name) then - begin - Inc(FileCount); - if FileCount >= 100 then - begin - Inc(FileCount); // For "... (more files)" entry - Break; - end; - end; - end; - until FindNext(SearchRec) <> 0; - finally - System.SysUtils.FindClose(SearchRec); - end; - end; - except - // Ignore file system errors during file enumeration - FileCount := 0; - end; - - ChildCount := DirCount + FileCount; -end; - -procedure TProjectManagerPanel.TreeViewFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode); -var - Data: PProjectNodeData; -begin - Data := Sender.GetNodeData(Node); - if Assigned(Data) then - begin - // Clear the record - no dynamic allocation to free - FillChar(Data^, SizeOf(TProjectNodeData), 0); - end; -end; - -procedure TProjectManagerPanel.TreeViewGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: TImageIndex); -var - Data: PProjectNodeData; -begin - // Only handle normal and selected images, ignore state images - if not (Kind in [ikNormal, ikSelected]) then - begin - ImageIndex := -1; - Exit; - end; - - // Safety check for shutdown - if IsShuttingDown then - begin - ImageIndex := -1; - Ghosted := False; - Exit; - end; - - Data := Sender.GetNodeData(Node); - if Assigned(Data) then - begin - case Data^.NodeType of - ntProject: - if (Data^.ProjectIndex >= 0) and (Data^.ProjectIndex < FProjectFolders.Count) then - begin - if System.SysUtils.DirectoryExists(FProjectFolders[Data^.ProjectIndex].Path) then - ImageIndex := TImageIndex(GetSystemImageIndex(FProjectFolders[Data^.ProjectIndex].Path)) - else - ImageIndex := TImageIndex(GetSystemImageIndex('')); // Default/error icon - end - else - ImageIndex := TImageIndex(GetSystemImageIndex('')); // Default/error icon - ntFolder: ImageIndex := TImageIndex(GetSystemImageIndex(Data^.FullPath)); - ntFile: ImageIndex := TImageIndex(GetSystemImageIndex(Data^.FullPath)); - else - ImageIndex := TImageIndex(GetSystemImageIndex('')); - end; - end - else - ImageIndex := TImageIndex(GetSystemImageIndex('')); - - Ghosted := False; -end; - -procedure TProjectManagerPanel.TreeViewDblClick(Sender: TObject); -var - Node: PVirtualNode; - Data: PProjectNodeData; - FileExt: string; -begin - Node := FTreeView.FocusedNode; - if Assigned(Node) then - begin - Data := FTreeView.GetNodeData(Node); - if Assigned(Data) and (Data^.NodeType = ntFile) and (Data^.FullPath <> '') then - begin - FileExt := LowerCase(ExtractFileExt(Data^.FullPath)); - - // Check if it's a supported SQL file type that can be opened in query tabs - if (FileExt = '.sql') or (FileExt = '.tsql') or (FileExt = '.pgsql') or - (FileExt = '.psql') or (FileExt = '.ddl') or (FileExt = '.dml') or - (FileExt = '.proc') or (FileExt = '.func') or (FileExt = '.view') or - (FileExt = '.trigger') or (FileExt = '.sp') or (FileExt = '.udf') then - begin - OpenFileInQueryTab(Data^.FullPath); - end - else if (FileExt = '.db') or (FileExt = '.sqlite') or (FileExt = '.sqlite3') then - begin - // For database files, we could potentially open them as connections - // For now, just inform the user - ErrorDialog(_('Database files should be opened via the connection manager.') + #13#10 + - _('Use File > Connect to Database to open this SQLite database.')); - end - else if IsTextFile(ExtractFileName(Data^.FullPath)) then - begin - // For other text files, open with default system editor - ShellExecute(0, 'open', PChar(Data^.FullPath), nil, nil, SW_SHOWNORMAL); - end - else - begin - ErrorDialog(f_('File type "%s" is not directly supported for editing.', [FileExt]) + #13#10 + - _('You can open it with the system default application via the context menu.')); - end; - end; - end; -end; - -procedure TProjectManagerPanel.TreeViewKeyPress(Sender: TObject; var Key: Char); -var - Node: PVirtualNode; - Data: PProjectNodeData; - FileExt: string; -begin - case Key of - #13: // Enter key - open file - begin - Node := FTreeView.FocusedNode; - if Assigned(Node) then - begin - Data := FTreeView.GetNodeData(Node); - if Assigned(Data) and (Data^.NodeType = ntFile) and (Data^.FullPath <> '') then - begin - FileExt := LowerCase(ExtractFileExt(Data^.FullPath)); - - // Check if it's a supported SQL file type that can be opened in query tabs - if (FileExt = '.sql') or (FileExt = '.tsql') or (FileExt = '.pgsql') or - (FileExt = '.psql') or (FileExt = '.ddl') or (FileExt = '.dml') or - (FileExt = '.proc') or (FileExt = '.func') or (FileExt = '.view') or - (FileExt = '.trigger') or (FileExt = '.sp') or (FileExt = '.udf') then - begin - OpenFileInQueryTab(Data^.FullPath); - Key := #0; // Consume the key - end; - end; - end; - end; - - 'F': // F key - open folder - begin - if GetKeyState(VK_CONTROL) < 0 then // Ctrl+F - begin - PopupOpenFolderClick(Sender); - Key := #0; - end; - end; - - 'C': // C key - copy path - begin - if GetKeyState(VK_CONTROL) < 0 then // Ctrl+C - begin - PopupCopyPathClick(Sender); - Key := #0; - end; - end; - - 'E': // E key - open with system editor - begin - if GetKeyState(VK_CONTROL) < 0 then // Ctrl+E - begin - PopupOpenWithSystemEditorClick(Sender); - Key := #0; - end; - end; - - #46: // Delete key - remove project (only for project nodes) - begin - Node := FTreeView.FocusedNode; - if Assigned(Node) then - begin - Data := FTreeView.GetNodeData(Node); - if Assigned(Data) and (Data^.NodeType = ntProject) then - begin - BtnRemoveProjectClick(Sender); - Key := #0; - end; - end; - end; - - #113: // F2 key - rename project - begin - Node := FTreeView.FocusedNode; - if Assigned(Node) then - begin - Data := FTreeView.GetNodeData(Node); - if Assigned(Data) and (Data^.NodeType = ntProject) then - begin - BtnRenameProjectClick(Sender); - Key := #0; - end; - end; - end; - end; -end; - -procedure TProjectManagerPanel.PopupOpenFileClick(Sender: TObject); -var - Node: PVirtualNode; - Data: PProjectNodeData; - FileExt: string; -begin - Node := FTreeView.FocusedNode; - if Assigned(Node) then - begin - Data := FTreeView.GetNodeData(Node); - if Assigned(Data) and (Data^.NodeType = ntFile) and (Data^.FullPath <> '') then - begin - FileExt := LowerCase(ExtractFileExt(Data^.FullPath)); - - // Check if it's a supported SQL file type - if (FileExt = '.sql') or (FileExt = '.tsql') or (FileExt = '.pgsql') or - (FileExt = '.psql') or (FileExt = '.db') or (FileExt = '.sqlite') or - (FileExt = '.sqlite3') then - begin - OpenFileInQueryTab(Data^.FullPath); - end - else - begin - // Unsupported file type - ErrorDialog(f_('File type "%s" is not currently supported.', [FileExt]) + #13#10 + - _('Supported file types:') + #13#10 + - '• .sql - ' + _('Standard SQL scripts') + #13#10 + - '• .tsql - ' + _('Transact-SQL for SQL Server') + #13#10 + - '• .pgsql/.psql - ' + _('PostgreSQL scripts') + #13#10 + - '• .db/.sqlite/.sqlite3 - ' + _('SQLite database files')); - end; - end; - end; -end; - -procedure TProjectManagerPanel.PopupOpenFolderClick(Sender: TObject); -var - Node: PVirtualNode; - Data: PProjectNodeData; - FolderPath: string; -begin - Node := FTreeView.FocusedNode; - if Assigned(Node) then - begin - Data := FTreeView.GetNodeData(Node); - if Assigned(Data) then - begin - case Data^.NodeType of - ntProject: - if (Data^.ProjectIndex >= 0) and (Data^.ProjectIndex < FProjectFolders.Count) then - FolderPath := FProjectFolders[Data^.ProjectIndex].Path - else - FolderPath := ''; - ntFolder: FolderPath := Data^.FullPath; - ntFile: FolderPath := ExtractFilePath(Data^.FullPath); - end; - - if System.SysUtils.DirectoryExists(FolderPath) then - ShellExecute(0, 'explore', PChar(FolderPath), nil, nil, SW_SHOWNORMAL) - else - ErrorDialog(f_('Folder not found: %s', [FolderPath])); - end; - end; -end; - -procedure TProjectManagerPanel.PopupOpenWithSystemEditorClick(Sender: TObject); -var - Node: PVirtualNode; - Data: PProjectNodeData; -begin - Node := FTreeView.FocusedNode; - if Assigned(Node) then - begin - Data := FTreeView.GetNodeData(Node); - if Assigned(Data) and (Data^.NodeType = ntFile) and (Data^.FullPath <> '') then - begin - if FileExists(Data^.FullPath) then - ShellExecute(0, 'open', PChar(Data^.FullPath), nil, nil, SW_SHOWNORMAL) - else - ErrorDialog(f_('File not found: %s', [Data^.FullPath])); - end; - end; -end; - -procedure TProjectManagerPanel.PopupCopyPathClick(Sender: TObject); -var - Node: PVirtualNode; - Data: PProjectNodeData; - PathToCopy: string; -begin - Node := FTreeView.FocusedNode; - if Assigned(Node) then - begin - Data := FTreeView.GetNodeData(Node); - if Assigned(Data) then - begin - case Data^.NodeType of - ntProject: - if (Data^.ProjectIndex >= 0) and (Data^.ProjectIndex < FProjectFolders.Count) then - PathToCopy := FProjectFolders[Data^.ProjectIndex].Path - else - PathToCopy := ''; - ntFolder: PathToCopy := Data^.FullPath; - ntFile: PathToCopy := Data^.FullPath; - end; - - if PathToCopy <> '' then - begin - try - Vcl.Clipbrd.Clipboard.AsText := PathToCopy; - except - on E: Exception do - ErrorDialog(_('Failed to copy path to clipboard: ') + E.Message); - end; - end; - end; - end; -end; - -procedure TProjectManagerPanel.PopupRemoveProjectClick(Sender: TObject); -begin - BtnRemoveProjectClick(Sender); -end; - -procedure TProjectManagerPanel.PopupRenameProjectClick(Sender: TObject); -begin - BtnRenameProjectClick(Sender); -end; - -function TProjectManagerPanel.InitProjectsIniFile: TIniFile; -var - WaitingSince: UInt64; - Attempts: Integer; - ProjectsIniFilename: String; -begin - // Try to open projects.ini for writing or reading - // Taking multiple application instances into account - if AppSettings.PortableMode then - ProjectsIniFilename := ExtractFilePath(Application.ExeName) + 'projects.ini' - else - ProjectsIniFilename := AppSettings.DirnameUserAppData + 'projects.ini'; - WaitingSince := GetTickCount64; - Attempts := 0; - while not FileIsWritable(ProjectsIniFilename) do begin - if GetTickCount64 - WaitingSince > 3000 then - Raise Exception.Create(f_('Could not open file %s', [ProjectsIniFilename])); - Sleep(200); - Inc(Attempts); - end; - if Attempts > 0 then begin - // Could log this, but we'll skip it for now - end; - // Catch errors when file cannot be created - if not FileExists(ProjectsIniFilename) then begin - SaveUnicodeFile(ProjectsIniFilename, '', UTF8NoBOMEncoding); - end; - Result := TIniFile.Create(ProjectsIniFilename); -end; - -procedure TProjectManagerPanel.LoadProjects; -var - ProjectsIni: TIniFile; - Sections: TStringList; - Section: String; - ProjectFolder: TProjectFolder; - ProjectName, ProjectPath: String; -begin - FProjectFolders.Clear; - - try - ProjectsIni := InitProjectsIniFile; - - Sections := TStringList.Create; - try - ProjectsIni.ReadSections(Sections); - - for Section in Sections do - begin - ProjectName := ProjectsIni.ReadString(Section, 'Name', ''); - ProjectPath := ProjectsIni.ReadString(Section, 'Path', ''); - - if (ProjectName <> '') and (ProjectPath <> '') then - begin - ProjectFolder := TProjectFolder.Create(ProjectName, ProjectPath); - FProjectFolders.Add(ProjectFolder); - end; - end; - finally - Sections.Free; - end; - - ProjectsIni.Free; - except - on E: Exception do - begin - // Ignore loading errors, start with empty project list - // Could log this: 'Error loading projects: ' + E.Message - end; - end; -end; - -procedure TProjectManagerPanel.SaveProjects; -var - ProjectsIni: TIniFile; - i: Integer; - Section: String; - Sections: TStringList; -begin - // Safety check - don't save if we're shutting down or objects are invalid - if not Assigned(FProjectFolders) or (FProjectFolders.Count = 0) then - Exit; - - try - ProjectsIni := InitProjectsIniFile; - - try - // Clear all existing sections first - Sections := TStringList.Create; - try - ProjectsIni.ReadSections(Sections); - for Section in Sections do - ProjectsIni.EraseSection(Section); - finally - Sections.Free; - end; - - // Write all projects - for i := 0 to FProjectFolders.Count - 1 do - begin - // Safety check for each project - if not Assigned(FProjectFolders[i]) then - Continue; - - Section := 'Project' + IntToStr(i); - ProjectsIni.WriteString(Section, 'Name', FProjectFolders[i].Name); - ProjectsIni.WriteString(Section, 'Path', FProjectFolders[i].Path); - end; - finally - ProjectsIni.Free; - end; - except - on E: Exception do - begin - // During shutdown, don't show message boxes as they can cause access violations - // Could log this: 'Error saving projects: ' + E.Message - if not (csDestroying in ComponentState) then - ErrorDialog(_('Error saving projects: ') + E.Message); - end; - end; -end; - -procedure TProjectManagerPanel.RefreshProjectTree; -var - WasExpanded: array of Boolean; - i, j: Integer; - Node: PVirtualNode; -begin - // Safety check - don't refresh if we're shutting down or objects are invalid - if not Assigned(FTreeView) or not Assigned(FProjectFolders) or (csDestroying in ComponentState) then - Exit; - - try - // Store expansion state before clearing - SetLength(WasExpanded, FProjectFolders.Count); - for i := 0 to FProjectFolders.Count - 1 do - begin - Node := FTreeView.GetFirstChild(FTreeView.RootNode); - // Skip to the i-th child - for j := 0 to i - 1 do - begin - if Assigned(Node) then - Node := FTreeView.GetNextSibling(Node); - end; - WasExpanded[i] := Assigned(Node) and (vsExpanded in Node.States); - end; - - FTreeView.Clear; - - if FProjectFolders.Count = 0 then - begin - // Add placeholder nodes for empty state - FTreeView.RootNodeCount := 2; - end - else - begin - FTreeView.RootNodeCount := FProjectFolders.Count; - - // Restore expansion state - for i := 0 to Min(FProjectFolders.Count - 1, Length(WasExpanded) - 1) do - begin - if WasExpanded[i] then - begin - Node := FTreeView.GetFirstChild(FTreeView.RootNode); - // Skip to the i-th child - for j := 0 to i - 1 do - begin - if Assigned(Node) then - Node := FTreeView.GetNextSibling(Node); - end; - if Assigned(Node) then - FTreeView.Expanded[Node] := True; - end; - end; - end; - except - // Suppress any errors during tree refresh to prevent access violations - end; -end; - -function TProjectManagerPanel.IsTextFile(const FileName: string): Boolean; -var - Ext: string; -begin - Ext := LowerCase(ExtractFileExt(FileName)); - Result := (Ext = '.sql') or (Ext = '.tsql') or (Ext = '.pgsql') or - (Ext = '.psql') or (Ext = '.db') or (Ext = '.sqlite') or - (Ext = '.sqlite3') or (Ext = '.ddl') or (Ext = '.dml') or - (Ext = '.proc') or (Ext = '.func') or (Ext = '.view') or - (Ext = '.trigger') or (Ext = '.sp') or (Ext = '.udf') or - (Ext = '.txt') or (Ext = '.md') or (Ext = '.readme') or - (Ext = '.log') or (Ext = '.csv') or (Ext = '.json') or - (Ext = '.xml') or (Ext = '.yml') or (Ext = '.yaml'); -end; - -function TProjectManagerPanel.IsFileOpenInTab(const FilePath: string): Boolean; -var - i: Integer; -begin - Result := False; - for i := 0 to MainForm.QueryTabs.Count - 1 do - begin - if SameText(MainForm.QueryTabs[i].MemoFilename, FilePath) then - begin - Result := True; - Break; - end; - end; -end; - -procedure TProjectManagerPanel.TreeViewPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType); -var - Data: PProjectNodeData; - WindowColor: TColor; - Brightness: Integer; -begin - // Safety check for shutdown - if IsShuttingDown then - Exit; - - Data := Sender.GetNodeData(Node); - if Assigned(Data) then - begin - if (Data^.NodeType = ntFile) and (Data^.FullPath <> '') then - begin - // Handle file nodes with open-file highlighting - if IsFileOpenInTab(Data^.FullPath) then - begin - // Make text bold - TargetCanvas.Font.Style := TargetCanvas.Font.Style + [fsBold]; - - // Calculate brightness of the background to choose appropriate text color - WindowColor := GetThemeColor(clWindow); - - // Calculate brightness using weighted RGB values (perceived brightness formula) - Brightness := Round((GetRValue(WindowColor) * 0.299) + - (GetGValue(WindowColor) * 0.587) + - (GetBValue(WindowColor) * 0.114)); - - // Choose color based on background brightness - if Brightness > 128 then - // Light background - use dark blue - TargetCanvas.Font.Color := RGB(0, 100, 200) - else - // Dark background - use light blue - TargetCanvas.Font.Color := RGB(100, 200, 255); - end; - end; - end; -end; - -procedure TProjectManagerPanel.UpdateTreeAppearance; -begin - // Just invalidate the tree to trigger a repaint with updated file states - if Assigned(FTreeView) and not (csDestroying in ComponentState) then - begin - try - FTreeView.Invalidate; - except - // Suppress any errors during tree invalidation - end; - end; -end; - -procedure TProjectManagerPanel.OpenFileInQueryTab(const FilePath: string); -var - Tab: TQueryTab; - i: Integer; - ExistingTab: TQueryTab; -begin - try - if not FileExists(FilePath) then - begin - ErrorDialog(f_('File not found: %s', [FilePath])); - Exit; - end; - - // First, check if the file is already open in an existing tab - ExistingTab := nil; - for i := 0 to MainForm.QueryTabs.Count - 1 do - begin - if SameText(MainForm.QueryTabs[i].MemoFilename, FilePath) then - begin - ExistingTab := MainForm.QueryTabs[i]; - Break; - end; - end; - - if Assigned(ExistingTab) then - begin - // File is already open, just focus the existing tab - MainForm.SetMainTab(ExistingTab.TabSheet); - end - else - begin - // File is not open, create new tab and load the file - Tab := MainForm.GetOrCreateEmptyQueryTab(True); // True = focus the tab - - // Load the file content into the tab - if not Tab.LoadContents(FilePath, True, nil) then - begin - ErrorDialog(f_('Failed to load file: %s', [FilePath])); - end - else - begin - // Update tree appearance to highlight the newly opened file - UpdateTreeAppearance; - end; - end; - - except - on E: Exception do - begin - ErrorDialog(_('Error opening file: ') + E.Message); - end; - end; -end; - -function TProjectManagerPanel.IsShuttingDown: Boolean; -begin - Result := (csDestroying in ComponentState) or - not Assigned(FTreeView) or - not Assigned(FProjectFolders); -end; - -procedure TProjectManagerPanel.ToggleVisibility; -begin - Visible := not Visible; - // Also toggle associated splitter if it exists - if Assigned(Parent) then - begin - // Find the splitter in the same parent - var i: Integer; - for i := 0 to Parent.ControlCount - 1 do - begin - if (Parent.Controls[i] is TSplitter) and - (TSplitter(Parent.Controls[i]).Align = alBottom) then - begin - TSplitter(Parent.Controls[i]).Visible := Visible; - Break; - end; - end; - end; -end; - -end.