Files
HeidiSQL/source/projectmanager.pas
Raphael Tweitmann 09ce6a8def Synchronizes Project Manager tab state across all query tabs
Implements global state management for Project Manager tab selection that persists across all query tabs and application sessions.

Adds new setting to remember user's preferred tab selection and ensures consistent behavior when switching between query tabs or opening new ones.

Replaces per-tab toggle behavior with synchronized state management where changing the active tab in one query tab automatically updates all others to maintain consistency.
2025-08-17 22:42:49 +02:00

1716 lines
54 KiB
ObjectPascal

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<TProjectFolder>;
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<TProjectFolder>.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.