unit u_main;

{$I u_defines.inc}

interface

uses
  Classes, SysUtils, LazFileUtils, SynEditKeyCmds, SynHighlighterLFM, Forms,
  StdCtrls, AnchorDocking, AnchorDockStorage, AnchorDockOptionsDlg, Controls,
  Graphics, strutils, Dialogs, Menus, ActnList, ExtCtrls, process,
  {$IFDEF WINDOWS}Windows, {$ENDIF} XMLPropStorage, SynExportHTML,
  fpjson, jsonscanner, LCLIntf,
  u_common, u_ceproject, u_synmemo, u_writableComponent, u_simpleget, u_compilers,
  u_widget, u_messages, u_interfaces, u_editor, u_projinspect, u_ceprojeditor,
  u_search, u_miniexplorer, u_libman, u_libmaneditor, u_todolist, u_observer,
  u_toolseditor, u_procinput, u_optionseditor, u_symlist, u_mru, u_processes,
  u_infos, u_dubproject, u_dialogs, u_dubprojeditor, u_gdb,
  u_dfmt, u_lcldragdrop, u_projgroup, u_projutils, u_stringrange, u_dexed_d,
  u_halstead, u_profileviewer, u_semver, u_dsgncontrols, u_term, u_newdubproj;

type

  TApplicationOptions = class;

  TLifetimeProvider = class(ILifetimeManager)
  strict private
    fStatus: TLifetimeStatus;
    function singleServiceName: string;
    function getLifetimeStatus: TLifetimeStatus;
    function asObject: TObject;
  public
    constructor create;
    property lifetimeStatus: TLifetimeStatus read fStatus write fStatus;
  end;

  TAsyncWait = (awNo, awYes, awCustom);

  TRunnableToFolderCondition = (
    ifInProject,  // runnable src is part of the project
    ifNotSaved,   // runnable src is an unsaved module (tmp_XXXXX)
    ifSaved       // runnable src not in project but saved not in temp dir
  );

  TRunnablesToFolderConditions = set of TRunnableToFolderCondition;

  TRunnableOptions = class(TWritableLfmTextComponent)
  private
    fCompiler: DCompiler;
    fDetectMain: boolean;
    fDetectLibraries: boolean;
    fOutputFolder: TPathname;
    fAlwaysToFolder: boolean;
    fStaticSwitches: TStringList;
    fToFolderConditions: TRunnablesToFolderConditions;
    procedure setOutputFolder(const value: TPathname);
    procedure setStaticSwitches(value: TStringList);
    procedure setCompiler(value: DCompiler);
  protected
    procedure afterLoad; override;
  published
    property alwaysToFolder: boolean read fAlwaysToFolder write fAlwaysToFolder stored false; deprecated;
    property compiler: DCompiler read fCompiler write setCompiler;
    property detectMain: boolean read fDetectMain write fDetectMain;
    property detectLibraries: boolean read fDetectLibraries write fDetectLibraries;
    property outputFolder: TPathname read fOutputFolder write setOutputFolder;
    property outputFolderConditions: TRunnablesToFolderConditions read fToFolderConditions write fToFolderConditions;
    property staticSwitches: TStringList read fStaticSwitches write setStaticSwitches;
  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
    procedure assign(source: TPersistent); override;
    procedure setDefaultSwitches;
    procedure sanitizeSwitches;
  end;

  TEditableRunnableOptions = class(TRunnableOptions, IEditableOptions)
  private
    fBackup: TRunnableOptions;
    function optionedWantCategory(): string;
    function optionedWantEditorKind: TOptionEditorKind;
    function optionedWantContainer: TPersistent;
    procedure optionedEvent(event: TOptionEditorEvent);
    function optionedOptionsModified: boolean;
  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
  end;

  { TMainForm }
  TMainForm = class(TForm, IDocumentObserver, IEditableShortCut, IProjectObserver, IMainMenu)
    actFileCompAndRun: TAction;
    actFileSaveAll: TAction;
    actFileClose: TAction;
    actFileAddToProj: TAction;
    actFileNewRun: TAction;
    actFileNew: TAction;
    actFileOpen: TAction;
    actFileSaveAs: TAction;
    actFileSave: TAction;
    actFileCompAndRunWithArgs: TAction;
    actEdFind: TAction;
    actEdFindNext: TAction;
    actFileOpenContFold: TAction;
    actFileHtmlExport: TAction;
    actFileUnittest: TAction;
    actFileCompileAndRunOut: TAction;
    actFileSaveCopyAs: TAction;
    actFileCompile: TAction;
    actFileRun: TAction;
    actFileDscanner: TAction;
    actFileRunOut: TAction;
    actFileRunDub: TAction;
    actFileRunDubOut: TAction;
    actFileNewDubScript: TAction;
    actFileMetricsHalstead: TAction;
    actFileCloseAllOthers: TAction;
    actFileCloseAll: TAction;
    actFileNewClip: TAction;
    actEdFormat: TAction;
    actProjSetEnv: TAction;
    actProjGitPull: TAction;
    actProjGitBranchesUpd: TAction;
    actProjNewDialog: TAction;
    actProjStopComp: TAction;
    actProjTest: TAction;
    actLayoutReset: TAction;
    actProjDscan: TAction;
    actProjGroupCompileCustomSync: TAction;
    actProjGroupClose: TAction;
    actProjGroupCompileSync: TAction;
    actProjGroupCompile: TAction;
    actProjSelUngrouped: TAction;
    actProjAddToGroup: TAction;
    actProjNewGroup: TAction;
    actProjOpenGroup: TAction;
    actProjSaveGroup: TAction;
    actProjSaveGroupAs: TAction;
    actProjNewDubJson: TAction;
    actProjNewNative: TAction;
    actSetRunnableSw: TAction;
    actLayoutSave: TAction;
    actProjOpenContFold: TAction;
    actProjOptView: TAction;
    actProjSource: TAction;
    actProjRun: TAction;
    actProjRunWithArgs: TAction;
    actProjCompile: TAction;
    actProjCompileAndRun: TAction;
    actProjCompAndRunWithArgs: TAction;
    actProjClose: TAction;
    actProjEditor: TAction;
    actProjOpen: TAction;
    actProjSave: TAction;
    actProjSaveAs: TAction;
    actEdMacPlay: TAction;
    actEdMacStartStop: TAction;
    actEdCut: TAction;
    actEdRedo: TAction;
    actEdUndo: TAction;
    actEdPaste: TAction;
    actEdCopy: TAction;
    actEdIndent: TAction;
    actEdUnIndent: TAction;
    Actions: TActionList;
    ApplicationProperties1: TApplicationProperties;
    mainMenu: TMainMenu;
    MenuItem1: TMenuItem;
    MenuItem10: TMenuItem;
    MenuItem100: TMenuItem;
    MenuItem101: TMenuItem;
    MenuItem102: TMenuItem;
    MenuItem103: TMenuItem;
    MenuItem104: TMenuItem;
    MenuItem105: TMenuItem;
    MenuItem106: TMenuItem;
    MenuItem107: TMenuItem;
    MenuItem108: TMenuItem;
    MenuItem109: TMenuItem;
    MenuItem110: TMenuItem;
    MenuItem111: TMenuItem;
    MenuItem112: TMenuItem;
    MenuItem113: TMenuItem;
    MenuItem114: TMenuItem;
    MenuItem115: TMenuItem;
    MenuItem116: TMenuItem;
    MenuItem117: TMenuItem;
    mnuGitBranch: TMenuItem;
    mnuItemDubDialog: TMenuItem;
    mnuItemHelp: TMenuItem;
    mnuItemAbout: TMenuItem;
    mnuItemCheckUpd: TMenuItem;
    mnuItemManual: TMenuItem;
    MenuItem31: TMenuItem;
    MenuItem76: TMenuItem;
    MenuItem77: TMenuItem;
    mnuOpts: TMenuItem;
    mnuItemMruGroup: TMenuItem;
    MenuItem11: TMenuItem;
    MenuItem12: TMenuItem;
    MenuItem13: TMenuItem;
    MenuItem14: TMenuItem;
    MenuItem15: TMenuItem;
    MenuItem16: TMenuItem;
    MenuItem17: TMenuItem;
    MenuItem18: TMenuItem;
    MenuItem19: TMenuItem;
    MenuItem2: TMenuItem;
    MenuItem20: TMenuItem;
    MenuItem21: TMenuItem;
    MenuItem22: TMenuItem;
    MenuItem23: TMenuItem;
    MenuItem24: TMenuItem;
    MenuItem25: TMenuItem;
    MenuItem26: TMenuItem;
    MenuItem27: TMenuItem;
    MenuItem28: TMenuItem;
    MenuItem29: TMenuItem;
    MenuItem3: TMenuItem;
    MenuItem30: TMenuItem;
    mnuProjNew: TMenuItem;
    MenuItem32: TMenuItem;
    MenuItem33: TMenuItem;
    MenuItem34: TMenuItem;
    MenuItem35: TMenuItem;
    MenuItem36: TMenuItem;
    MenuItem37: TMenuItem;
    MenuItem38: TMenuItem;
    MenuItem39: TMenuItem;
    MenuItem40: TMenuItem;
    MenuItem41: TMenuItem;
    MenuItem42: TMenuItem;
    MenuItem43: TMenuItem;
    MenuItem44: TMenuItem;
    MenuItem45: TMenuItem;
    MenuItem46: TMenuItem;
    MenuItem47: TMenuItem;
    MenuItem48: TMenuItem;
    MenuItem49: TMenuItem;
    MenuItem50: TMenuItem;
    MenuItem51: TMenuItem;
    MenuItem52: TMenuItem;
    MenuItem53: TMenuItem;
    MenuItem54: TMenuItem;
    MenuItem55: TMenuItem;
    MenuItem56: TMenuItem;
    MenuItem57: TMenuItem;
    MenuItem58: TMenuItem;
    MenuItem59: TMenuItem;
    MenuItem60: TMenuItem;
    MenuItem61: TMenuItem;
    MenuItem62: TMenuItem;
    MenuItem63: TMenuItem;
    MenuItem64: TMenuItem;
    MenuItem65: TMenuItem;
    MenuItem66: TMenuItem;
    MenuItem67: TMenuItem;
    MenuItem68: TMenuItem;
    MenuItem69: TMenuItem;
    MenuItem70: TMenuItem;
    MenuItem71: TMenuItem;
    MenuItem72: TMenuItem;
    MenuItem73: TMenuItem;
    MenuItem74: TMenuItem;
    MenuItem75: TMenuItem;
    MenuItem78: TMenuItem;
    MenuItem79: TMenuItem;
    MenuItem80: TMenuItem;
    MenuItem81: TMenuItem;
    MenuItem82: TMenuItem;
    MenuItem83: TMenuItem;
    MenuItem84: TMenuItem;
    MenuItem85: TMenuItem;
    MenuItem86: TMenuItem;
    MenuItem87: TMenuItem;
    MenuItem88: TMenuItem;
    MenuItem89: TMenuItem;
    MenuItem90: TMenuItem;
    MenuItem91: TMenuItem;
    MenuItem92: TMenuItem;
    MenuItem93: TMenuItem;
    MenuItem94: TMenuItem;
    MenuItem95: TMenuItem;
    MenuItem96: TMenuItem;
    MenuItem97: TMenuItem;
    MenuItem98: TMenuItem;
    MenuItem99: TMenuItem;
    mnuLayout: TMenuItem;
    mnuItemMruFile: TMenuItem;
    mnuItemMruProj: TMenuItem;
    mnuItemWin: TMenuItem;
    MenuItem4: TMenuItem;
    MenuItem5: TMenuItem;
    MenuItem6: TMenuItem;
    MenuItem7: TMenuItem;
    MenuItem8: TMenuItem;
    MenuItem9: TMenuItem;
    procedure actEdFormatExecute(Sender: TObject);
    procedure actFileCloseAllExecute(Sender: TObject);
    procedure actFileCloseAllOthersExecute(Sender: TObject);
    procedure actFileCompileExecute(Sender: TObject);
    procedure actFileDscannerExecute(Sender: TObject);
    procedure actFileMetricsHalsteadExecute(Sender: TObject);
    procedure actFileNewClipExecute(Sender: TObject);
    procedure actFileNewDubScriptExecute(Sender: TObject);
    procedure actFileRunDubExecute(Sender: TObject);
    procedure actFileRunDubOutExecute(Sender: TObject);
    procedure actFileRunExecute(Sender: TObject);
    procedure actFileRunOutExecute(Sender: TObject);
    procedure actFileSaveCopyAsExecute(Sender: TObject);
    procedure actLayoutResetExecute(Sender: TObject);
    procedure actNewGroupExecute(Sender: TObject);
    procedure actProjAddToGroupExecute(Sender: TObject);
    procedure actProjDscanExecute(Sender: TObject);
    procedure actProjGitBranchesUpdExecute(Sender: TObject);
    procedure actProjGitPullExecute(Sender: TObject);
    procedure actProjGroupCompileCustomSyncExecute(Sender: TObject);
    procedure actProjGroupCompileExecute(Sender: TObject);
    procedure actProjGroupCompileSyncExecute(Sender: TObject);
    procedure actProjNewDialogExecute(Sender: TObject);
    procedure actProjNewDubJsonExecute(Sender: TObject);
    procedure actProjNewGroupExecute(Sender: TObject);
    procedure actProjNewNativeExecute(Sender: TObject);
    procedure actProjOpenGroupExecute(Sender: TObject);
    procedure actProjSaveGroupAsExecute(Sender: TObject);
    procedure actProjSaveGroupExecute(Sender: TObject);
    procedure actProjSelUngroupedExecute(Sender: TObject);
    procedure actProjSetEnvExecute(Sender: TObject);
    procedure actProjStopCompExecute(Sender: TObject);
    procedure actProjTestExecute(Sender: TObject);
    procedure actSetRunnableSwExecute(Sender: TObject);
    procedure ApplicationProperties1Activate(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormResize(Sender: TObject);
    procedure mnuGitBranchClick(Sender: TObject);
    procedure mnuItemAboutClick(Sender: TObject);
    procedure mnuItemCheckUpdClick(Sender: TObject);
    procedure mnuItemManualClick(Sender: TObject);
    procedure updateDocumentBasedAction(sender: TObject);
    procedure updateProjectBasedAction(sender: TObject);
    procedure updateDocEditBasedAction(sender: TObject);
    procedure actFileCompileAndRunOutExecute(Sender: TObject);
    procedure actEdFindExecute(Sender: TObject);
    procedure actEdFindNextExecute(Sender: TObject);
    procedure actFileAddToProjExecute(Sender: TObject);
    procedure actFileCloseExecute(Sender: TObject);
    procedure actFileCompAndRunExecute(Sender: TObject);
    procedure actFileCompAndRunWithArgsExecute(Sender: TObject);
    procedure actFileHtmlExportExecute(Sender: TObject);
    procedure actFileOpenContFoldExecute(Sender: TObject);
    procedure actFileSaveAllExecute(Sender: TObject);
    procedure actEdIndentExecute(Sender: TObject);
    procedure actFileUnittestExecute(Sender: TObject);
    procedure actLayoutSaveExecute(Sender: TObject);
    procedure actProjCompAndRunWithArgsExecute(Sender: TObject);
    procedure actProjCompileAndRunExecute(Sender: TObject);
    procedure actProjCompileExecute(Sender: TObject);
    procedure actEdCopyExecute(Sender: TObject);
    procedure actEdCutExecute(Sender: TObject);
    procedure actEdMacPlayExecute(Sender: TObject);
    procedure actEdMacStartStopExecute(Sender: TObject);
    procedure actFileNewExecute(Sender: TObject);
    procedure actFileNewRunExecute(Sender: TObject);
    procedure actFileOpenExecute(Sender: TObject);
    procedure actProjOpenContFoldExecute(Sender: TObject);
    procedure actProjOpenExecute(Sender: TObject);
    procedure actEdPasteExecute(Sender: TObject);
    procedure actProjCloseExecute(Sender: TObject);
    procedure actProjEditorExecute(Sender: TObject);
    procedure actEdRedoExecute(Sender: TObject);
    procedure actFileSaveAsExecute(Sender: TObject);
    procedure actFileSaveExecute(Sender: TObject);
    procedure actProjOptViewExecute(Sender: TObject);
    procedure actProjRunExecute(Sender: TObject);
    procedure actProjRunWithArgsExecute(Sender: TObject);
    procedure actProjSaveAsExecute(Sender: TObject);
    procedure actProjSaveExecute(Sender: TObject);
    procedure actEdUndoExecute(Sender: TObject);
    procedure actProjSourceExecute(Sender: TObject);
    procedure actEdUnIndentExecute(Sender: TObject);
    procedure ApplicationProperties1Exception(Sender: TObject; E: Exception);
    procedure FormCloseQuery(Sender: TObject; var CanClose: boolean);
    procedure FormDropFiles(Sender: TObject; const fnames: array of string);

  protected

    procedure DoFirstShow; override;

  private

    fDockingIsInitialized: boolean;
    fGitIconIndex: integer;
    fCleanIconIndex: integer;
    fImages: TImageList;
    fOptionCategories: TEditableOptionsSubject;
    fRunnablesOptions: TEditableRunnableOptions;
    fSymStringExpander: ISymStringExpander;
    fProjectGroup: IProjectGroup;
    fCovModUt: boolean;
    fDscanUnittests: boolean;
    fDoc: TDexedMemo;
    fFirstTimeRun: boolean;
    fMultidoc: IMultiDocHandler;
    fProcInputHandler: IProcInputHandler;
    fUpdateCount: PtrInt;
    fProj: ICommonProject;
    fFreeProj: ICommonProject;
    fProjBeforeGroup: ICommonProject;
    fDubProject: TDubProject;
    fNativeProject: TNativeProject;
    fProjMru: TMRUProjectList;
    fFileMru: TMRUDocumentList;
    fPrjGrpMru: TMRUProjectsGroupList;
    fWidgList: TWidgetList;
    fMesgWidg: TMessagesWidget;
    fEditWidg: TEditorWidget;
    fProjWidg: TProjectInspectWidget;
    fPrjCfWidg: TProjectConfigurationWidget;
    fFindWidg:  TSearchWidget;
    fExplWidg: TMiniExplorerWidget;
    fLibMWidg: TLibManEditorWidget;
    fTlsEdWidg: TToolsEditorWidget;
    fPrInpWidg: TProcInputWidget;
    fTodolWidg: TTodoListWidget;
    fOptEdWidg: TOptionEditorWidget;
    fSymlWidg: TSymbolListWidget;
    fInfoWidg: TInfoWidget;
    fDubProjWidg: TDubProjectEditorWidget;
    fPrjGrpWidg: TProjectGroupWidget;
    fGdbWidg: TGdbWidget;
    {$IFDEF UNIX}
    fTermWWidg: TTermWidget;
    {$ENDIF}

    fDfmtWidg: TDfmtWidget;
    fProfWidg: TProfileViewerWidget;
    fCompStart: UInt64;

    fRunProjAfterCompArg: boolean;
    fRunProjAfterCompile: boolean;
    fIsCompilingGroup: boolean;
    fGroupCompilationCnt: integer;
    fProjFromCommandLine: boolean;
    fInitialized: boolean;
    fRunProc: TDexedProcess;
    fMsgs: IMessagesDisplay;
    fAppliOpts: TApplicationOptions;
    fProjActionsLock: boolean;
    fCompilerSelector: ICompilerSelector;
    fLifeTimeStatusProvider: TLifetimeProvider;
    procedure updateFloatingWidgetOnTop(onTop: boolean);
    procedure widgetDockingChanged(sender: TDexedWidget; newState: TWidgetDockingState);
    procedure mnuOptsItemClick(sender: TObject);

    // IMainMenu
    function singleServiceName: string;
    function mnuAdd: TMenuItem;
    procedure mnuDelete(value: TMenuItem);

    // IDocumentObserver
    procedure docNew(document: TDexedMemo);
    procedure docClosing(document: TDexedMemo);
    procedure docFocused(document: TDexedMemo);
    procedure docChanged(document: TDexedMemo);

    // IProjectObserver
    procedure projNew(project: ICommonProject);
    procedure projChanged(project: ICommonProject);
    procedure projClosing(project: ICommonProject);
    procedure projFocused(project: ICommonProject);
    procedure projCompiling(project: ICommonProject);
    procedure projCompiled(project: ICommonProject; success: boolean);

    // IEditableShortCut
    function  scedCount: integer;
    function  scedGetItem(const index: integer): TEditableShortcut;
    procedure scedSetItem(const index: integer; constref item: TEditableShortcut);

    //Init - Fina
    procedure InitImages;
    procedure processCmdlineParams;
    procedure InitMRUs;
    procedure InitWidgets;
    procedure InitDocking(reset: boolean = false);
    procedure InitOptionsMenu;
    procedure LoadSettings;
    procedure SaveSettings;
    function LoadDocking: boolean;
    procedure SaveDocking;
    procedure LoadLastDocsAndProj;
    procedure SaveLastDocsAndProj;
    procedure FreeRunnableProc;

    // widget interfaces subroutines
    procedure updateWidgetMenuEntry(sender: TObject);
    procedure widgetShowFromAction(sender: TObject);
    procedure snapTopSplitterToMenu;

    // run & exec sub routines
    function runnableExename: string;
    procedure asyncprocOutput(sender: TObject);
    procedure asyncprocTerminate(sender: TObject);
    procedure unittestDone(Sender: TObject);
    function  compileRunnable(unittest: boolean = false): boolean;
    procedure executeRunnable(unittest: boolean = false; redirect: boolean = true;
      const runArgs: string = '');
    procedure runFile(outside: boolean);
    procedure dubFile(outside: boolean);

    // file sub routines
    procedure newFile;
    procedure saveFile(document: TDexedMemo);
    procedure openFile(const fname: string);

    // project sub routines
    procedure saveProjSource(const document: TDexedMemo);
    procedure newNativeProj;
    procedure newDubProj;
    procedure saveProj;
    procedure saveProjAs(const fname: string);
    procedure openProj(const fname: string);
    function closeProj: boolean;
    procedure showProjTitle;
    function  checkProjectLock(message: boolean = true): boolean;
    procedure compileGroup(async: TAsyncWait);

    // mru
    procedure mruChange(Sender: TObject);
    procedure mruFileItemClick(Sender: TObject);
    procedure mruProjItemClick(Sender: TObject);
    procedure mruProjGroupItemClick(Sender: TObject);
    procedure mruClearClick(Sender: TObject);
    procedure mruClearInvalidClick(Sender: TObject);

    // layout
    procedure setSplitterWheelEvent;
    procedure DockSplitterMw(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
    procedure LockTopWindow(Sender: TObject; var NewSize: Integer; var Accept: Boolean);
    procedure layoutMnuItemClick(sender: TObject);
    procedure layoutLoadFromFile(const fname: string);
    procedure layoutSaveToFile(const fname: string);
    procedure layoutUpdateMenu;

    // git
    procedure gitBranchMenuItemClick(sender: TObject);

  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
    procedure UpdateDockCaption(Exclude: TControl = nil); override;
  end;

  TPersistentMainShortcuts = class(TWritableLfmTextComponent)
  private
    fCol: TCollection;
    procedure setCol(value: TCollection);
  published
    property shortcut: TCollection read fCol write setCol;
  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
    //
    procedure assign(source: TPersistent); override;
    procedure assignTo(target: TPersistent); override;
  end;

  TPersistentMainMrus = class(TWritableLfmTextComponent)
  private
    fFileMruPt: TMRUFileList;
    fProjMruPt: TMRUFileList;
    fPrjGrpMruPt: TMRUFileList;
    procedure setProjMru(value: TMRUFileList);
    procedure setFileMru(value: TMRUFileList);
    procedure setProjectsGroupMru(value: TMRUFileList);
  published
    property mostRecentFiles: TMRUFileList read fFileMruPt write setFileMru;
    property mostRecentprojects: TMRUFileList read fProjMruPt write setProjMru;
    property mostRecentProjectsGroups: TMRUFileList read fPrjGrpMruPt write setProjectsGroupMru;
  public
    procedure setTargets(projs: TMRUFileList; files: TMRUFileList; group: TMRUFileList);
  end;

  TLastDocsAndProjs = class(TWritableLfmTextComponent)
  private
    fDocuments: TStringList;
    fProjName: string;
    fDocIndex: integer;
    fProjectGroup: string;
    fProjectIndex: integer;
    procedure setDocuments(value: TStringList);
  protected
    procedure beforeSave; override;
    procedure afterLoad; override;
  published
    property documentIndex: integer read fDocIndex write fDocIndex;
    property documents: TStringList read fDocuments write setDocuments;
    property project: string read fProjName write fProjName;
    property projectGroup: string read fProjectGroup write fProjectGroup;
    property projectIndex: integer read fProjectIndex write fProjectIndex;
  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
    procedure Assign(source: TPersistent); override;
    procedure AssignTo(target: TPersistent); override;
  end;

  TApplicationOptionsBase = class(TWritableLfmTextComponent)
  private
    fFloatingWidgetOnTop: boolean;
    fReloadLastDocuments: boolean;
    fCovModUt: boolean;
    fMaxRecentProjs: integer;
    fMaxRecentDocs: integer;
    fMaxRecentGroups: integer;
    fDcdPort: word;
    fDscanUnittests: boolean;
    fAutoSaveProjectFiles: boolean;
    fFlatLook: boolean;
    fSplitterScrollSpeed: byte;
    fAutoCheckUpdates: boolean;
    fShowBuildDuration: boolean;
    fToolBarScaling: TToolBarScaling;
    fAutoKillProcThreshold: dword;
    fGlobalCompiler: DCompiler;
    fAutoCleanMRU: boolean;
    fMinimizeDlangMemory: TEditEvent;
    function getConsoleProgram: string;
    procedure setConsoleProgram(const value: string);
    function getAdditionalPATH: string;
    procedure setAdditionalPATH(const value: string);
    function getNativeProjecCompiler: DCompiler;
    procedure setNativeProjecCompiler(value: DCompiler);
    procedure setSplitterScsrollSpeed(value: byte);
    procedure setMinimizeDlangMemory(value: TEditEvent);
  published
    property additionalPATH: string read getAdditionalPATH write setAdditionalPath;
    property autoCheckUpdates: boolean read fAutoCheckUpdates write fAutoCheckUpdates;
    property autoCleanMRU: boolean read fAutoCleanMRU write fAutoCleanMRU default true;
    property autoKillProcThreshold: dword read fAutoKillProcThreshold write fAutoKillProcThreshold default 1024 * 1024 * 2;
    property consoleProgram: string read getConsoleProgram write setConsoleProgram;
    property coverModuleTests: boolean read fCovModUt write fCovModUt;
    property floatingWidgetOnTop: boolean read fFloatingWidgetOnTop write fFloatingWidgetOnTop;
    property reloadLastDocuments: boolean read fReloadLastDocuments write fReloadLastDocuments;
    property maxRecentProjects: integer read fMaxRecentProjs write fMaxRecentProjs;
    property maxRecentDocuments: integer read fMaxRecentDocs write fMaxRecentDocs;
    property maxRecentProjectsGroups: integer read fMaxRecentGroups write fMaxRecentGroups;
    property nativeProjectCompiler: DCompiler read getNativeProjecCompiler write setNativeProjecCompiler;
    property dscanUnittests: boolean read fDscanUnittests write fDscanUnittests default true;
    property autoSaveProjectFiles: boolean read fAutoSaveProjectFiles write fAutoSaveProjectFiles default false;
    property flatLook: boolean read fFlatLook write fFlatLook;
    property splitterScrollSpeed: byte read fSplitterScrollSpeed write setSplitterScsrollSpeed;
    property showBuildDuration: boolean read fShowBuildDuration write fShowBuildDuration default false;
    property globalCompiler: DCompiler write fGlobalCompiler; deprecated;
    // this only display a button that has for effect to free unused D GC mem.
    property minimizeDlangMemory: TEditEvent read fMinimizeDlangMemory write setMinimizeDlangMemory stored false;
    // property toolBarScaling: TToolBarScaling read fToolBarScaling write fToolBarScaling stored false;
    // published for IEditableOptions but stored by DCD wrapper since it reloads before MainForm
    property dcdPort: word read fDcdPort write fDcdPort stored false;
  end;

  TApplicationOptions = class(TApplicationOptionsBase, IEditableOptions)
  private
    fBackup:TApplicationOptionsBase;
    //
    function optionedWantCategory(): string;
    function optionedWantEditorKind: TOptionEditorKind;
    function optionedWantContainer: TPersistent;
    procedure optionedEvent(event: TOptionEditorEvent);
    function optionedOptionsModified: boolean;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure assign(source: TPersistent); override;
    procedure assignTo(target: TPersistent); override;
  end;

var
  MainForm: TMainForm;

implementation
{$R *.lfm}

uses
  SynMacroRecorder, u_dcd, openssl;

{$REGION TRunnableOptions ----------------------------------------------------}
constructor TRunnableOptions.create(aOwner: TComponent);
begin
  inherited;
  fStaticSwitches := TStringList.create;
  fStaticSwitches.Duplicates := TDuplicates.dupIgnore;
  fStaticSwitches.Sorted:=true;
end;


destructor TRunnableOptions.destroy;
begin
  fStaticSwitches.free;
  inherited;
end;

procedure TRunnableOptions.assign(source: TPersistent);
var
  src: TRunnableOptions;
begin
  if source is TRunnableOptions then
  begin
    src := TRunnableOptions(source);
    fCompiler:= src.fCompiler;
    fDetectMain:= src.fDetectMain;
    fDetectLibraries:= src.fDetectLibraries;
    fOutputFolder:= src.fOutputFolder;
    fToFolderConditions:= src.fToFolderConditions;
    fStaticSwitches.assign(src.fStaticSwitches);
  end
  else inherited;
end;

procedure TRunnableOptions.setStaticSwitches(value: TStringList);
begin
  fStaticSwitches.Assign(value);
  sanitizeSwitches;
end;

procedure TRunnableOptions.afterLoad;
begin
  inherited;
  if fStaticSwitches.Count.equals(0) then
    setDefaultSwitches
  else
    sanitizeSwitches;
end;

procedure TRunnableOptions.setDefaultSwitches;
begin
  fStaticSwitches.Clear;
  fStaticSwitches.AddStrings(['-vcolumns', '-w', '-wi']);
  {$ifdef WINDOWS}
  {$ifdef CPUX86_64}
  fStaticSwitches.Add('-m64');
  {$endif}
  {$endif}
end;

procedure TRunnableOptions.sanitizeSwitches;
var
  i: integer;
  sw: string;
  lst: TStringList;
begin
  lst := TStringList.Create;
  try
    for i:= 0 to fStaticSwitches.Count-1 do
    begin
      sw := fStaticSwitches[i];
      RemovePadChars(sw, [#0..#32]);
      // not a switch
      if sw.length < 2 then
        continue
      else if sw[1] <> '-' then
        continue
      // set according to the context
      else if sw = '-unittest' then
        continue
      else if sw = '-main' then
        continue
      // would break location detection
      else if (sw.length > 2) and (sw[1..3] = '-of') then
        continue
      // useless
      else if sw = '-run' then
        continue
      else
        lst.Add(sw);
    end;
    fStaticSwitches.Assign(lst);
  finally
    lst.free;
  end;
end;

procedure TRunnableOptions.setOutputFolder(const value: TPathname);
begin
  fOutputFolder := value;
  if (length(fOutputFolder) > 0)
    and (fOutputFolder[length(fOutputFolder)] <> DirectorySeparator) then
      fOutputFolder += DirectorySeparator;
end;

procedure TRunnableOptions.setCompiler(value: DCompiler);
begin
  if fCompiler = value then
    exit;
  fCompiler := value;
  if not getCompilerSelector.isCompilerValid(fCompiler) then
    fCompiler := dmd;
  fCompiler :=value;
end;

constructor TEditableRunnableOptions.create(aOwner: TComponent);
begin
  inherited;
  fBackup := TRunnableOptions.create(nil);
  EntitiesConnector.addObserver(self);
end;

destructor TEditableRunnableOptions.destroy;
begin
  fBackup.free;
  EntitiesConnector.removeObserver(self);
  inherited;
end;

function TEditableRunnableOptions.optionedWantCategory(): string;
begin
  exit('Runnable modules');
end;

function TEditableRunnableOptions.optionedWantEditorKind: TOptionEditorKind;
begin
  exit(oekGeneric);
end;

function TEditableRunnableOptions.optionedWantContainer: TPersistent;
begin
  fBackup.assign(self);
  exit(self);
end;

procedure TEditableRunnableOptions.optionedEvent(event: TOptionEditorEvent);
begin
  case event of
    oeeAccept:
      begin
        fBackup.assign(self);
        sanitizeSwitches;
      end;
    oeeCancel: assign(fBackup);
    oeeSelectCat: fBackup.assign(self);
  end;
end;

function TEditableRunnableOptions.optionedOptionsModified: boolean;
begin
  exit(false);
end;
{$ENDREGION --------------------------------------------------------------------}

{$REGION TApplicationOptions -------------------------------------------------}
constructor TApplicationOptions.Create(AOwner: TComponent);
begin
  inherited;
  fBackup := TApplicationOptionsBase.Create(self);
  EntitiesConnector.addObserver(self);
  fDscanUnittests := true;
  fSplitterScrollSpeed := 2;
  fMaxRecentProjs := 10;
  fMaxRecentDocs := 10;
  fMaxRecentGroups:= 10;
  fReloadLastDocuments:=true;
  fFlatLook:=true;
  fDcdPort:=DCDWrapper.port;
  fAutoKillProcThreshold := 1024 * 1024 * 2;
  fAutoCleanMRU := true;
end;

function TApplicationOptionsBase.getNativeProjecCompiler: DCompiler;
begin
  exit(u_ceproject.getCEProjectCompiler);
end;

procedure TApplicationOptionsBase.setNativeProjecCompiler(value: DCompiler);
begin
  u_ceproject.setCEProjectCompiler(value);
end;

procedure TApplicationOptionsBase.setSplitterScsrollSpeed(value: byte);
begin
  if value < 1 then
    value := 1
  else if value > 10 then
    value := 10;
  fSplitterScrollSpeed:=value;
end;

procedure TApplicationOptionsBase.setMinimizeDlangMemory(value: TEditEvent);
begin
  minimizeGcHeap(true);
end;

function TApplicationOptionsBase.getAdditionalPATH: string;
begin
  exit(u_common.additionalPath);
end;

function TApplicationOptionsBase.getConsoleProgram: string;
begin
  result := u_common.consoleProgram;
end;

procedure TApplicationOptionsBase.setConsoleProgram(const value: string);
begin
  if exeFullName(value).fileExists then
    u_common.consoleProgram:=value;
end;

procedure TApplicationOptionsBase.setAdditionalPath(const value: string);
var
  str: TStringList;
  cat: string;
  i: integer;
begin
  str := TStringList.Create;
  try
    str.Delimiter:= PathSeparator;
    str.DelimitedText:= value;
    for i := str.Count-1 downto 0 do
      if not str[i].dirExists then
        str.Delete(i);
    cat := str.DelimitedText;
    u_common.additionalPath := cat;
  finally
    str.Free;
  end;
end;

destructor TApplicationOptions.Destroy;
begin
  EntitiesConnector.removeObserver(self);
  inherited;
end;

procedure TApplicationOptions.assign(source: TPersistent);
begin
  if source = MainForm then
  begin
    fMaxRecentProjs:= MainForm.fProjMru.maxCount;
    fMaxRecentDocs:= MainForm.fFileMru.maxCount;
    fMaxRecentGroups:= MainForm.fPrjGrpMru.maxCount;
    fDcdPort := DcdWrapper.port;
    fCovModUt:= MainForm.fCovModUt;
    fDscanUnittests := MainForm.fDscanUnittests;
  end else if source = fBackup then
  begin
    fCovModUt:=fBackup.fCovModUt;
    fDcdPort:=fBackup.fDcdPort;
    fMaxRecentDocs:= fBackup.fMaxRecentDocs;
    fMaxRecentProjs:= fBackup.fMaxRecentProjs;
    fMaxRecentGroups := fBackup.fMaxRecentGroups;
    fReloadLastDocuments:=fBackup.fReloadLastDocuments;
    fFloatingWidgetOnTop := fBackup.fFloatingWidgetOnTop;
    fShowBuildDuration:= fBackup.fShowBuildDuration;
    fAutoSaveProjectFiles:= fBackup.fAutoSaveProjectFiles;
    fdscanUnittests:= fBackup.dscanUnittests;
    fFlatLook:=fBackup.fFlatLook;
    fAutoCheckUpdates:= fBackup.fAutoCheckUpdates;
    MainForm.fDscanUnittests := fDscanUnittests;
    nativeProjectCompiler:= fBackup.nativeProjectCompiler;
    fToolBarScaling:= fBackup.fToolBarScaling;
    fAutoKillProcThreshold:= fBackup.fAutoKillProcThreshold;
  end
  else inherited;
end;

procedure TApplicationOptions.assignTo(target: TPersistent);
var
  i: integer;
begin
  if target = MainForm then
  begin
    MainForm.fCovModUt:= fCovModUt;
    MainForm.fProjMru.maxCount := fMaxRecentProjs;
    MainForm.fProjMru.removeNotExisting := fAutoCleanMRU;
    MainForm.fFileMru.maxCount := fMaxRecentDocs;
    MainForm.fFileMru.removeNotExisting:= fAutoCleanMRU;
    MainForm.fPrjGrpMru.maxCount:= fMaxRecentGroups;
    MainForm.updateFloatingWidgetOnTop(fFloatingWidgetOnTop);
    MainForm.fDscanUnittests := fDscanUnittests;
    TDexedProcess.autoKillProcThreshold:= fAutoKillProcThreshold;
    DcdWrapper.port:=fDcdPort;
    for i := 0 to MainForm.fWidgList.Count-1 do
    begin
      MainForm.fWidgList.widget[i].toolbarFlat:=fFlatLook;
      MainForm.fWidgList.widget[i].toolbar.Scaling:= fToolBarScaling;
    end;
  end
  else if target = fBackup then
  begin
    fBackup.fMaxRecentDocs:= fMaxRecentDocs;
    fBackup.fMaxRecentProjs:= fMaxRecentProjs;
    fBackup.fMaxRecentGroups:= fMaxRecentGroups;
    fBackup.fReloadLastDocuments:=fReloadLastDocuments;
    fBackup.fFloatingWidgetOnTop:=fFloatingWidgetOnTop;
    fBackup.fDcdPort:=fDcdPort;
    fBackup.fCovModUt:=fCovModUt;
    fBackup.fAutoSaveProjectFiles:= fAutoSaveProjectFiles;
    fBackup.fDscanUnittests:= fDscanUnittests;
    fBackup.fFlatLook:= fFlatLook;
    fBackup.fToolBarScaling:= fToolBarScaling;
    fBackup.fAutoCheckUpdates:= fAutoCheckUpdates;
    fBackup.fShowBuildDuration:= fShowBuildDuration;
    fBackup.nativeProjectCompiler:= nativeProjectCompiler;
    fBackup.fAutoKillProcThreshold := fAutoKillProcThreshold;
  end
  else inherited;
end;

function TApplicationOptions.optionedWantCategory(): string;
begin
  exit('Application');
end;

function TApplicationOptions.optionedWantEditorKind: TOptionEditorKind;
begin
  exit(oekGeneric);
end;

function TApplicationOptions.optionedWantContainer: TPersistent;
begin
  AssignTo(fBackup);
  exit(self);
end;

procedure TApplicationOptions.optionedEvent(event: TOptionEditorEvent);
begin
  case event of
    oeeCancel: begin Assign(fBackup); AssignTo(MainForm); end;
    oeeAccept: begin AssignTo(MainForm); AssignTo(fBackup);end;
    oeeSelectCat: begin Assign(MainForm); AssignTo(fBackup); end;
    oeeChange: AssignTo(MainForm);
  end;
end;

function TApplicationOptions.optionedOptionsModified: boolean;
begin
  exit(false);
end;
{$ENDREGION}

{$REGION TLastDocsAndProjs ---------------------------------------------------}
constructor TLastDocsAndProjs.create(aOwner: TComponent);
begin
  inherited;
  fDocuments := TStringList.Create;
end;

destructor TLastDocsAndProjs.destroy;
begin
  fDocuments.Free;
  inherited;
end;

procedure TLastDocsAndProjs.Assign(source: TPersistent);
var
  grp: IProjectGroup;
  prj: ICommonProject = nil;
  pix: integer;
begin
  if source = MainForm then
  begin
    grp := getProjectGroup;
    pix := grp.reloadedProjectIndex;
    prj := MainForm.fFreeProj;
    if prj.isAssigned then
      fProjName := prj.filename;
    fProjectGroup := getProjectGroup.groupFilename;
    if prj = MainForm.fProj then
      fProjectIndex := -1
    else
      fProjectIndex := pix;
  end else
    inherited;
end;

procedure TLastDocsAndProjs.AssignTo(target: TPersistent);
var
  dst: TMainForm;
  hdl: IMultiDocHandler;
  mem: TDexedMemo = nil;
  grp: IProjectGroup;
begin
  if target is TMainForm then
  begin
    dst := TMainForm(target);
    if dst.fProjFromCommandLine then
      exit;

    if fProjName.isNotEmpty and fProjName.fileExists then
    begin
      dst.openProj(fProjName);
      if dst.fProj.isNotAssigned then
        exit;
      hdl := getMultiDocHandler;
      if hdl.isAssigned then
        mem := hdl.findDocument(dst.fProj.filename);
      if mem.isAssigned then
      begin
        if dst.fProj.getFormat = pfDEXED then
          mem.Highlighter := LfmSyn
        else
          mem.Highlighter := JsSyn;
      end;
    end;

    grp := getProjectGroup;
    if fProjectGroup.isNotEmpty and fProjectGroup.fileExists then
      grp.openGroup(fProjectGroup);
    if fProjectIndex.equals(-1) and dst.fFreeProj.isAssigned then
      dst.fFreeProj.activate
    else if (fProjectIndex >= 0) and (grp.projectCount > 0)
      and (fProjectIndex < grp.projectCount) then
    begin
      grp.setProjectIndex(fProjectIndex);
      grp.getProject(grp.getProjectIndex).activate;
    end;
  end
  else inherited;
end;

procedure TLastDocsAndProjs.setDocuments(value: TStringList);
begin
  fDocuments.Assign(value);
end;

procedure TLastDocsAndProjs.beforeSave;
var
  i: integer;
  docHandler: IMultiDocHandler;
  document: TDexedMemo;
  str: string;
begin
  docHandler := getMultiDocHandler;
  if docHandler.isNotAssigned then
    exit;

  for i:= 0 to docHandler.documentCount-1 do
  begin
    document := docHandler.document[i];
    str := document.fileName;
    if (str <> document.tempFilename) and str.fileExists then
    begin
      fDocuments.Add(str);
      if document.Focused then
        documentIndex := i;
    end;
  end;
end;

procedure TLastDocsAndProjs.afterLoad;
var
  docHandler: IMultiDocHandler;
  str: string;
  focusedName: string = '';
  i: integer;
begin
  docHandler := getMultiDocHandler;
  if docHandler.isNotAssigned then
    exit;

  for i := 0 to fDocuments.Count-1 do
  begin
    str := fDocuments[i];
    if str.fileExists then
    begin
      docHandler.openDocument(str);
      if i = fDocIndex then
        focusedName := str;
    end;
  end;

  if focusedName.isNotEmpty then
    docHandler.openDocument(focusedName);
end;
{$ENDREGION}

{$REGION Lifetime}
constructor TLifetimeProvider.create;
begin
  EntitiesConnector.addSingleService(self);
end;

function TLifetimeProvider.singleServiceName: string;
begin
  result := 'ILifetimeManager';
end;

function TLifetimeProvider.getLifetimeStatus: TLifetimeStatus;
begin
  result := fStatus;
end;

function TLifetimeProvider.asObject: TObject;
begin
  result := self;
end;
{$ENDREGION}

{$REGION Actions shortcuts -----------------------------------------------------}
constructor TPersistentMainShortcuts.create(aOwner: TComponent);
begin
  inherited;
  fCol := TCollection.Create(TPersistentShortcut);
end;

destructor TPersistentMainShortcuts.destroy;
begin
  fCol.Free;
  inherited;
end;

procedure TPersistentMainShortcuts.setCol(value: TCollection);
begin
  fCol.Assign(value);
end;

procedure TPersistentMainShortcuts.assign(source: TPersistent);
var
  itm: TPersistentShortcut;
  i: Integer;
begin
  fCol.Clear;
  if source = MainForm then
    for i := 0 to MainForm.Actions.ActionCount-1 do
    begin
      if MainForm.Actions.Actions[i].Owner <> MainForm then
        continue;
      itm := TPersistentShortcut(fCol.Add);
      itm.shortcut := TAction(MainForm.Actions.Actions[i]).Shortcut;
      itm.actionName := MainForm.Actions.Actions[i].Name;
    end
  else inherited;
end;

procedure TPersistentMainShortcuts.assignTo(target: TPersistent);
var
  m: TPersistentShortcut;
  a: TAction;
  i: integer;
  j: integer;
begin
  if target = MainForm then
    for i:= 0 to fCol.Count-1 do
  begin
    m := TPersistentShortcut(fCol.Items[i]);
    for j := 0 to MainForm.Actions.ActionCount-1 do
    begin
      a := TAction(MainForm.Actions.Actions[j]);
      if a.Name = m.actionName then
      begin
        a.shortcut := m.shortcut;
        break;
      end;
    end;
  end
  else inherited;
end;
{$ENDREGION}

{$REGION TPersistentMainMrus -------------------------------------------------}
procedure TPersistentMainMrus.setProjMru(value: TMRUFileList);
begin
  fProjMruPt.assign(value);
end;

procedure TPersistentMainMrus.setFileMru(value: TMRUFileList);
begin
  fFileMruPt.assign(value);
end;

procedure TPersistentMainMrus.setProjectsGroupMru(value: TMRUFileList);
begin
  fPrjGrpMruPt.assign(value);
end;

procedure TPersistentMainMrus.setTargets(projs: TMRUFileList; files: TMRUFileList;
  group: TMRUFileList);
begin
  fFileMruPt := files;
  fProjMruPt := projs;
  fPrjGrpMruPt := group;
end;
{$ENDREGION}

{$REGION Standard Comp/Obj------------------------------------------------------}
constructor TMainForm.create(aOwner: TComponent);
begin
  fLifeTimeStatusProvider := TLifetimeProvider.create;
  fLifeTimeStatusProvider.lifetimeStatus:=lfsLoading;

  inherited create(aOwner);

  // provide defaults, necessary because not handled by docking.xml
  width := (Screen.Width div 3) * 2;
  height := (Screen.Height div 3) * 2;

  fOptionCategories := TEditableOptionsSubject.create;

  EntitiesConnector.addObserver(self);
  EntitiesConnector.addSingleService(self);

  InitImages;
  InitMRUs;
  InitWidgets;
  LoadSettings;
  layoutUpdateMenu;
  fMultidoc := getMultiDocHandler;
  OnDragDrop:= @ddHandler.DragDrop;
  OnDragOver:= @ddHandler.DragOver;

  EntitiesConnector.forceUpdate;
  fSymStringExpander:= getSymStringExpander;
  fProjectGroup := getProjectGroup;
  fCompilerSelector := getCompilerSelector;

  processCmdlineParams;
  fAppliOpts.assignTo(self);

  // waiting for interative mode working when piped:
  // https://github.com/dlang/dub/issues/1500
  mnuItemDubDialog.Visible:=false;

  InitOptionsMenu;

  mainMenu.Items.Remove(mnuItemHelp);
  mainMenu.Items.Add(mnuItemHelp);

  fProcInputHandler := getprocInputHandler;

  fInitialized := true;
end;

procedure TMainForm.processCmdlineParams;
var
  value: string;
  lst: TStringList;
begin
  if application.ParamCount > 0 then
  begin
    value := application.Params[1];
    if value.isNotEmpty then
    begin
      lst := TStringList.Create;
      try
        lst.DelimitedText := value;
        for value in lst do
        begin
          if value.isEmpty then
            continue;
          if isEditable(value.extractFileExt) then
            openFile(value)
          else if isValidNativeProject(value) or isValidDubProject(value) then
          begin
            // so far CE can only open 1 project at a time
            openProj(value);
            fProjFromCommandLine := true;
            break;
          end
        end;
      finally
        lst.Free;
      end;
    end;
  end;
  value := application.GetOptionValue('p', 'project');
  if value.isNotEmpty and value.fileExists then
    openProj(value);
  value := application.GetOptionValue('f', 'files');
  if value.isNotEmpty then
  begin
    lst := TStringList.Create;
    try
      lst.DelimitedText := value;
      for value in lst do
      begin
        if value.fileExists then
          openFile(value);
      end;
    finally
      lst.Free;
    end;
  end;
end;

procedure TMainForm.InitOptionsMenu;
var
  l: TStringList;
  i: integer;
  s: string;
  t: TMenuItem;
  e: IEditableOptions;
begin
  l := TStringList.Create;
  try
    for i := 0 to fOptionCategories.observersCount-1 do
    begin
      e := fOptionCategories.observers[i] as IEditableOptions;
      s := e.optionedWantCategory;
      {$PUSH} {$WARNINGS OFF}
      l.AddObject(s, TObject(e));
      {$POP}
    end;
    l.Sort;
    for i := 0 to l.Count-1 do
    begin
      t := TMenuItem.Create(self);
      t.Caption := l[i];
      t.Tag:= PtrInt(l.Objects[i]);
      t.onClick := @mnuOptsItemClick;
      mnuOpts.Add(t);
    end;
  finally
    l.free;
  end;
end;

procedure TMainForm.mnuOptsItemClick(sender: TObject);
var
  c: IEditableOptions;
begin
  c := IEditableOptions(TMenuItem(sender).Tag);
  getOptionsEditor.showOptionEditor(c);
end;

procedure TMainForm.InitMRUs;
begin
  fProjMru := TMRUProjectList.Create;
  fFileMru := TMRUDocumentList.Create;
  fPrjGrpMru:= TMRUProjectsGroupList.create;
  fProjMru.objectTag := mnuItemMruProj;
  fFileMru.objectTag := mnuItemMruFile;
  fPrjGrpMru.objectTag := mnuItemMruGroup;
  fProjMru.OnChange := @mruChange;
  fFileMru.OnChange := @mruChange;
  fPrjGrpMru.OnChange := @mruChange;
end;

procedure TMainForm.InitImages;
var
  c: TIconScaledSize;
  z: array[TIconScaledSize] of integer = (16, 24, 32);
  i: integer;

  function loadIcon(value: string): integer;
  const
    s: array[TIconScaledSize] of string = ('', '24', '32');
  begin
    result := fImages.AddResourceName(HINSTANCE, value + s[c]);
  end;

begin
  c := GetIconScaledSize;

  fImages := TImageList.Create(self);
  fImages.Width:= z[c];
  fImages.Height:= z[c];
  Actions.Images := fImages;
  mainMenu.Images := fImages;

  i := loadIcon('CROSS');
  actFileClose.ImageIndex:= i;
  actFileCloseAll.ImageIndex:= i;
  actFileCloseAllOthers.ImageIndex:= i;
  actProjClose.ImageIndex:= i;
  actProjGroupClose.ImageIndex:=i;

  i := loadIcon('ERROR_CHECKING');
  actFileDscanner.ImageIndex := i;
  actProjDscan.ImageIndex:= i;
  actFileMetricsHalstead.ImageIndex:=i;

  i := loadIcon('DISK');
  actFileSave.ImageIndex:= i;
  actProjSave.ImageIndex:= i;
  actProjSaveGroup.ImageIndex:=i;

  i := loadIcon('DISK_PEN');
  actFileSaveAs.ImageIndex:= i;
  actFileSaveCopyAs.ImageIndex:= i;
  actProjSaveAs.ImageIndex:= i;
  actProjSaveGroupAs.ImageIndex:= i;

  i := loadIcon('DISK_MULTIPLE');
  actFileSaveAll.ImageIndex := i;

  i := loadIcon('FOLDER_VERTICAL_DOCUMENT');
  actFileOpen.ImageIndex:= i;
  actProjOpen.ImageIndex:= i;
  actProjOpenGroup.ImageIndex:= i;
  mnuItemMruFile.ImageIndex:= i;
  mnuItemMruGroup.ImageIndex:= i;
  mnuItemMruProj.ImageIndex:= i;

  i := loadIcon('INFORMATION');
  mnuItemAbout.ImageIndex:= i;

  i := loadIcon('SCRIPT_GEAR');
  actFileNewRun.ImageIndex:= i;
  actFileCompAndRun.ImageIndex:= i;
  actFileCompAndRunWithArgs.ImageIndex:= i;
  actFileCompileAndRunOut.ImageIndex:= i;
  actFileCompile.ImageIndex:= i;
  actFileRun.ImageIndex:= i;
  actFileRunOut.ImageIndex:= i;
  actFileUnittest.ImageIndex:= i;

  i := loadIcon('DOCUMENT');
  actFileNew.ImageIndex:= i;
  mnuProjNew.ImageIndex:= i;
  actProjNewNative.ImageIndex:= i;
  actProjNewDubJson.ImageIndex:= i;
  actProjNewGroup.ImageIndex:= i;

  i := loadIcon('DUB');
  actFileNewDubScript.ImageIndex:= i;
  actFileRunDub.ImageIndex:= i;
  actFileRunDubOut.ImageIndex:= i;

  i := loadIcon('FOLDERS_EXPLORER');
  actFileOpenContFold.ImageIndex:= i;
  actProjOpenContFold.ImageIndex:= i;

  i := loadIcon('CUT');
  actEdCut.ImageIndex:= i;
  i := loadIcon('COPY');
  actEdCopy.ImageIndex:= i;
  i := loadIcon('PASTE');
  actEdPaste.ImageIndex:= i;
  actFileNewClip.ImageIndex:= i;
  i := loadIcon('ARROW_UNDO');
  actEdUndo.ImageIndex:= i;
  i := loadIcon('ARROW_REDO');
  actEdRedo.ImageIndex:= i;
  i := loadIcon('FIND');
  actEdFind.ImageIndex:= i;
  actEdFindNext.ImageIndex:= i;

  i := loadIcon('SYSTEM_RUN');
  actProjCompile.ImageIndex:= i;
  actProjCompileAndRun.ImageIndex:= i;
  actProjCompAndRunWithArgs.ImageIndex:= i;
  actProjGroupCompile.ImageIndex:= i;
  actProjGroupCompileCustomSync.ImageIndex:= i;
  actProjGroupCompileSync.ImageIndex:= i;

  i := loadIcon('FLASH');
  actProjRun.ImageIndex:= i;
  actProjRunWithArgs.ImageIndex:= i;

  i := loadIcon('CHECK_BOXES_SERIES');
  actProjTest.ImageIndex:=i;

  i := loadIcon('LAYOUT');
  mnuLayout.ImageIndex:= i;

  i := LoadIcon('INDENT_MORE');
  actEdIndent.ImageIndex:= i;

  i := LoadIcon('INDENT_LESS');
  actEdUnIndent.ImageIndex:= i;

  i := LoadIcon('HTML_GO');
  actFileHtmlExport.ImageIndex:=i;

  i := LoadIcon('MOVE_TO_FOLDER');
  actFileAddToProj.ImageIndex:=i;

  i := loadIcon('CROSS');
  actProjStopComp.ImageIndex:=i;

  i := loadIcon('GIT');
  fGitIconIndex := i;
  mnuGitBranch.ImageIndex:=i;
  actProjGitPull.ImageIndex:=i;

  i := loadIcon('ARROW_UPDATE');
  actProjGitBranchesUpd.ImageIndex:=i;

  fCleanIconIndex := loadIcon('CLEAN');
end;

procedure TMainForm.InitWidgets;
var
  widg: TDexedWidget;
  act: TAction;
  itm: TMenuItem;
  idx: integer;
begin
  fWidgList   := TWidgetList.Create;
  fMesgWidg   := TMessagesWidget.create(self);
  fEditWidg   := TEditorWidget.create(self);
  fProjWidg   := TProjectInspectWidget.create(self);
  fPrjCfWidg  := TProjectConfigurationWidget.create(self);
  fFindWidg   := TSearchWidget.create(self);
  fExplWidg   := TMiniExplorerWidget.create(self);
  fLibMWidg   := TLibManEditorWidget.create(self);
  fTlsEdWidg  := TToolsEditorWidget.create(self);
  fPrInpWidg  := TProcInputWidget.create(self);
  fTodolWidg  := TTodoListWidget.create(self);
  fOptEdWidg  := TOptionEditorWidget.create(self);
  fSymlWidg   := TSymbolListWidget.create(self);
  fInfoWidg   := TInfoWidget.create(self);
  fDubProjWidg:= TDubProjectEditorWidget.create(self);
  fDfmtWidg   := TDfmtWidget.create(self);
  fPrjGrpWidg := TProjectGroupWidget.create(self);
  fProfWidg   := TProfileViewerWidget.create(self);
  fGdbWidg    := TGdbWidget.create(self);
  {$IFDEF UNIX}
  fTermWWidg  := TTermWidget.create(self);
  {$ENDIF}

  getMessageDisplay(fMsgs);

  fWidgList.Capacity := 32;
  fWidgList.addWidget(@fMesgWidg);
  fWidgList.addWidget(@fEditWidg);
  fWidgList.addWidget(@fProjWidg);
  fWidgList.addWidget(@fPrjCfWidg);
  fWidgList.addWidget(@fFindWidg);
  fWidgList.addWidget(@fExplWidg);
  fWidgList.addWidget(@fLibMWidg);
  fWidgList.addWidget(@fTlsEdWidg);
  fWidgList.addWidget(@fPrInpWidg);
  fWidgList.addWidget(@fTodolWidg);
  fWidgList.addWidget(@fOptEdWidg);
  fWidgList.addWidget(@fSymlWidg);
  fWidgList.addWidget(@fInfoWidg);
  fWidgList.addWidget(@fDubProjWidg);
  fWidgList.addWidget(@fDfmtWidg);
  fWidgList.addWidget(@fPrjGrpWidg);
  fWidgList.addWidget(@fProfWidg);
  fWidgList.addWidget(@fGdbWidg);
  {$IFDEF UNIX}
  fWidgList.addWidget(@fTermWWidg);
  {$ENDIF}

  fWidgList.sort(@CompareWidgCaption);

  case GetIconScaledSize of
    iss16: idx := fImages.AddResourceName(HINSTANCE, 'APPLICATION');
    iss24: idx := fImages.AddResourceName(HINSTANCE, 'APPLICATION24');
    iss32: idx := fImages.AddResourceName(HINSTANCE, 'APPLICATION32');
  end;

  for widg in fWidgList do
  begin
    act := TAction.Create(self);
    act.Category := 'Window';
    act.Caption := widg.Caption;
    act.OnExecute := @widgetShowFromAction;
    act.Tag := ptrInt(widg);
    act.ImageIndex := idx;
    act.OnUpdate:= @updateWidgetMenuEntry;
    itm := TMenuItem.Create(self);
    itm.Action := act;
    itm.Tag := ptrInt(widg);
    mnuItemWin.Add(itm);
  end;
end;

procedure TMainForm.LockTopWindow(Sender: TObject; var NewSize: Integer;
  var Accept: Boolean);
begin
  //TODO-cdocking: top splitter pos can change even if locked (e.g after resize)
  accept := GetKeyShiftState = [ssCtrl];
end;

procedure TMainForm.DockSplitterMw(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
  offs: integer;
  splt: TAnchorDockSplitter;
begin
  offs := -240 * fAppliOpts.splitterScrollSpeed div WheelDelta;
  splt := TAnchorDockSplitter(sender);
  splt.MoveSplitter(offs);
  if splt.ResizeAnchor in [akLeft, akRight] then
    Mouse.CursorPos:= classes.Point(Mouse.CursorPos.X + offs, Mouse.CursorPos.Y)
  else
    Mouse.CursorPos:= classes.Point(Mouse.CursorPos.X, Mouse.CursorPos.Y + offs);
  Handled := true;
end;

procedure TMainForm.setSplitterWheelEvent;
var
  i: integer;
  widg: TDexedWidget;
  site: TControl;
  anchl: TAnchorKind;
begin
  if csDestroying in ComponentState then
    exit;
  for i := 0 to fWidgList.Count-1 do
  begin
    widg := fWidgList.widget[i];
    if not widg.isDockable then
      continue;
    for anchl in [low(anchl) .. high(anchl)] do
      if GetDockSplitterOrParent(DockMaster.GetSite(widg), anchl, site) then
      begin
        if site is TAnchorDockHostSite then
        begin
          if TAnchorDockHostSite(site).BoundSplitter.isAssigned then
            TAnchorDockHostSite(site).BoundSplitter.OnMouseWheel:= @DockSplitterMw;
        end
        else if site is TAnchorDockSplitter then
          TAnchorDockSplitter(site).OnMouseWheel:= @DockSplitterMw;
      end;
  end;
end;

procedure TMainForm.widgetDockingChanged(sender: TDexedWidget; newState: TWidgetDockingState);
begin
  setSplitterWheelEvent;
end;

procedure TMainForm.InitDocking(reset: boolean = false);
var
  i: Integer;
  w: TDexedWidget;
  s: TAnchorDockSplitter;
  h: TAnchorDockHostSite;
begin

  fDockingIsInitialized := true;

  if not reset then
  begin
    DockMaster.MakeDockSite(Self, [akBottom], admrpChild);
    DockMaster.OnShowOptions := @ShowAnchorDockOptions;
    DockMaster.HeaderStyle := adhsPoints;
    DockMaster.HideHeaderCaptionFloatingControl := true;
    // makes widget dockable
    for i := 0 to fWidgList.Count-1 do
    begin
      w := fWidgList.widget[i];
      if not w.isDockable then
        continue;
      DockMaster.MakeDockable(w, true);
      DockMaster.GetAnchorSite(w).Header.HeaderPosition := adlhpTop;
      w.onDockingChanged:= @widgetDockingChanged;
    end;
  end;

  // load existing or default docking
  if not reset and FileExists(getDocPath + 'docking.xml') then
  begin
    // load later (https://bugs.freepascal.org/view.php?id=29475)
  end
  else
  begin
    if reset then
    begin
      for i := 0 to fWidgList.Count-1 do
      begin
        w := fWidgList.widget[i];
        if not w.isDockable then
          continue;
        if not w.Visible then
          continue;
        if w = fEditWidg then
          continue;
        h := DockMaster.GetAnchorSite(w);
        if h.isAssigned then
          h.ManualFloat(w.ClientRect, false);
      end;
    end;

    if not reset then
    begin
      Height := 0;
    end
    else
    begin
      if WindowState = wsMaximized then
        WindowState:= wsNormal;
      Height := 600;
      Width  := 800;
    end;

    // center
    if not reset then
      DockMaster.ManualDock(DockMaster.GetAnchorSite(fEditWidg), DockMaster.GetSite(Self), alBottom);

    DockMaster.ManualDock(DockMaster.GetAnchorSite(fMesgWidg), DockMaster.GetSite(fEditWidg), alBottom);
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fLibMWidg), DockMaster.GetSite(fMesgWidg), alClient, fMesgWidg);
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fTodolWidg), DockMaster.GetSite(fMesgWidg), alClient, fMesgWidg);
    {$IFDEF LINUX}
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fTermWWidg), DockMaster.GetSite(fMesgWidg), alClient, fMesgWidg);
    {$ENDIF}
    fMesgWidg.showWidget;
    // left
    DockMaster.GetAnchorSite(fSymlWidg).Width := 120;
    DockMaster.GetAnchorSite(fFindWidg).Width := 120;
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fSymlWidg), DockMaster.GetSite(fEditWidg), alLeft);
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fFindWidg), DockMaster.GetAnchorSite(fSymlWidg), alBottom, fSymlWidg);
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fPrInpWidg), DockMaster.GetAnchorSite(fFindWidg), alTop, fFindWidg);
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fExplWidg), DockMaster.GetSite(fSymlWidg), alClient, fSymlWidg);
    if GetDockSplitter(DockMaster.GetSite(fPrInpWidg), akTop, s) then
    begin
      s.MoveSplitter(50);
      s := nil;
    end;
    fSymlWidg.showWidget;
    // right
    DockMaster.GetAnchorSite(fProjWidg).Width := 190;
    DockMaster.GetAnchorSite(fDubProjWidg).Width := 190;
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fProjWidg), DockMaster.GetSite(fEditWidg), alRight);
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fPrjGrpWidg), DockMaster.GetSite(fProjWidg), alBottom, fProjWidg);
    DockMaster.ManualDock(DockMaster.GetAnchorSite(fDubProjWidg), DockMaster.GetAnchorSite(fProjWidg), alClient, fProjWidg);
    fProjWidg.showWidget;
    // close remaining and header to top
    for i := 0 to fWidgList.Count-1 do
    begin
      w := fWidgList.widget[i];
      if not w.isDockable then
        continue;
      DockMaster.GetAnchorSite(w).Header.HeaderPosition := adlhpTop;
      if not DockMaster.GetAnchorSite(w).HasParent then
        DockMaster.GetAnchorSite(w).Close;
    end;
    WindowState:= wsMaximized;
  end;
end;

procedure TMainForm.LoadSettings;
var
  fname: string;
begin
  // project and files MRU
  fname := getDocPath + 'mostrecent.txt';
  if fname.fileExists then
    with TPersistentMainMrus.create(nil) do
  try
    setTargets(fFileMru, fProjMru, fPrjGrpMru);
    loadFromFile(fname);
  finally
    Free;
  end;
  // shortcuts for the actions standing in the main action list
  fname := getDocPath + 'mainshortcuts.txt';
  if fname.fileExists then
    with TPersistentMainShortcuts.create(nil) do
  try
    loadFromFile(fname);
    assignTo(self);
  finally
    Free;
  end;
  // runnables opts
  fRunnablesOptions := TEditableRunnableOptions.create(self);
  fname := getDocPath + 'runnables.txt';
  if fname.fileExists then
    fRunnablesOptions.loadFromFile(fname);
  // globals opts
  fAppliOpts := TApplicationOptions.Create(self);
  fname := getDocPath + 'application.txt';
  if fname.fileExists then
  begin
    fAppliOpts.loadFromFile(fname);
    fAppliOpts.assignTo(self);
  end
  else fFirstTimeRun := true;
end;

procedure TMainForm.SaveSettings;
begin
  if not fInitialized then
    exit;
  // project and files MRU
  with TPersistentMainMrus.create(nil) do
  try
    setTargets(fFileMru, fProjMru, fPrjGrpMru);
    saveToFile(getDocPath + 'mostrecent.txt');
  finally
    Free;
  end;
  // shortcuts for the actions standing in the main action list
  with TPersistentMainShortcuts.create(nil) do
  try
    assign(self);
    saveToFile(getDocPath + 'mainshortcuts.txt');
  finally
    Free;
  end;
  // globals opts
  fAppliOpts.assign(self);
  fAppliOpts.saveToFile(getDocPath + 'application.txt');
  // runnables opts
  fRunnablesOptions.saveToFile(getDocPath + 'runnables.txt');
end;

procedure TMainForm.SaveDocking;
var
  x: TXMLConfigStorage;
  i: integer;
begin
  if not fInitialized or not Visible then
    exit;

  DockMaster.RestoreLayouts.Clear;
  if WindowState = wsMinimized then
    WindowState := wsNormal;
  // does not save minimized/undocked windows to prevent bugs
  for i:= 0 to fWidgList.Count-1 do
  begin
    if not fWidgList.widget[i].isDockable then
      continue;
    if DockMaster.GetAnchorSite(fWidgList.widget[i]).WindowState = wsMinimized then
      DockMaster.GetAnchorSite(fWidgList.widget[i]).Close
    else if not DockMaster.GetAnchorSite(fWidgList.widget[i]).HasParent then
      DockMaster.GetAnchorSite(fWidgList.widget[i]).Close;
  end;

  forceDirectory(getDocPath);
  x := TXMLConfigStorage.Create(getDocPath + 'docking.xml', false);
  try
    DockMaster.SaveLayoutToConfig(x);
    x.WriteToDisk;
  finally
    x.Free;
  end;

  x := TXMLConfigStorage.Create(getDocPath + 'dockingopts.xml',false);
  try
    DockMaster.SaveSettingsToConfig(x);
    x.WriteToDisk;
  finally
    x.Free;
  end;
end;

function TMainForm.LoadDocking: boolean;
var
  x: TXMLConfigStorage;
  s: TMemoryStream;
  f: string;
  w: TDexedWidget;
begin
  result := false;
  f := getDocPath + 'docking.xml';
  if fileExists(f) then
  begin
    x := TXMLConfigStorage.Create(f, true);
    try
      try
        // without this the relaoding fails
        // see https://bugs.freepascal.org/view.php?id=34454
        for w in fWidgList do
          DockMaster.ManualFloat(w);
        DockMaster.LoadLayoutFromConfig(x, false);
      except
        exit;
      end;
      s := TMemoryStream.Create;
      try
        x.SaveToStream(s);
        s.saveToFile(f)
      finally
        s.Free;
      end;
    finally
      x.Free;
    end;
  end;
  if fileExists(getDocPath + 'dockingopts.xml') then
  begin
    x := TXMLConfigStorage.Create(getDocPath + 'dockingopts.xml', true);
    try
      DockMaster.LoadSettingsFromConfig(x);
    finally
      x.Free;
    end;
  end;
  result := true;
end;

procedure TMainForm.FreeRunnableProc;
var
  f: string;
begin
  if fRunProc.isNotAssigned then
    exit;

  f := fRunProc.Executable;
  if fProcInputHandler.process = fRunProc  then
  begin
    getMessageDisplay.message('the execution of a runnable module ' +
      'has been implicitly aborted', fDoc, amcEdit, amkWarn);
    fProcInputHandler.addProcess(nil);
  end;
  killProcess(fRunProc);
  if f.fileExists and (f.extractFilePath = GetTempDir(false)) then
    sysutils.DeleteFile(f);
end;

procedure TMainForm.SaveLastDocsAndProj;
begin
  with TLastDocsAndProjs.create(nil) do
  try
    assign(self);
    saveToFile(getDocPath + 'lastdocsandproj.txt');
  finally
    free;
  end;
end;

procedure TMainForm.LoadLastDocsAndProj;
var
  str: string;
begin
  str := getDocPath + 'lastdocsandproj.txt';
  if str.fileExists then
    with TLastDocsAndProjs.create(nil) do
  try
    loadFromFile(str);
    assignTo(self);
  finally
    free;
  end;
end;

function checkForUpdate: string;
const
  updURL = 'https://gitlab.com/api/v4/projects/15908229/repository/tags';
var
  arr: TJSONArray = nil;
  dat: TJSONData = nil;
  tgg: TJSONData = nil;
  url: string;
  str: string = '';
  lst: TStringList = nil;
  res: TResourceStream = nil;
  svo: TSemVer;
  sva: TSemVer;
begin
  result := '';
  if simpleGet(updURL, dat) and (dat.JSONType = jtArray) then
  begin
    try
      arr := TJSONArray(dat);
      if (arr.Count > 0) and (arr.Items[0].JSONType = jtObject) then
      begin
        dat := arr.Objects[0];
        tgg := dat.FindPath('name');
      end;
      if tgg.isAssigned then
      begin
        url := 'https://gitlab.com/basile.b/dexed/-/releases/' + tgg.AsString;
        res:= TResourceStream.Create(HINSTANCE, 'VERSION', RT_RCDATA);
        lst := TstringList.Create;
        lst.LoadFromStream(res);
        str := lst.Text;
        sva.init(str, false);
        str := tgg.AsString;
        svo.init(str, false);
        if svo.valid and sva.valid and (svo > sva) then
          result := url
        else
          dlgOkInfo('No new version available');
      end;
    finally
      dat.free;
      lst.free;
      res.free;
    end;
  end
  else dlgOkError('Impossible to check new versions, ' + simpleGetErrMsg);
end;

procedure TMainForm.DoFirstShow;
var
  url: string;
begin
  inherited;

  InitDocking;

  // TODO-cbetterfix: clipboard doesn't work first time it's used on a reloaded doc.
  // see: http://forum.lazarus.freepascal.org/index.php/topic,30616.0.htm
  if fAppliOpts.reloadLastDocuments then
    LoadLastDocsAndProj;
  if fProj.isNotAssigned then
    newDubProj;

  DockMaster.ResetSplitters;
  setSplitterWheelEvent;

  if fFirstTimeRun then
  begin
    actFileNewRun.Execute;
    if fInfoWidg.hasMissingTools then
      fInfoWidg.showWidget;
  end;

  // see https://bugs.freepascal.org/view.php?id=29475
  // reloading must be done here otherwise there are "jumps"
  if FileExists(getDocPath + 'docking.xml') then
    LoadDocking();

  if fAppliOpts.autoCheckUpdates then
  begin
    url := checkForUpdate;
    if url <> '' then
    begin
      if dlgYesNo('A new version is available, do you wish to visit the release page ?' +
        lineEnding + '(' + url +')') = mrYes then
          OpenURL(url);
    end;
  end;

  fLifeTimeStatusProvider.lifetimeStatus := lfsLoaded;
end;

procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  // saving doesnt work when csDestroying in comp.state (i.e in destroy)
  if CloseAction = caFree then
    SaveDocking;
end;

procedure TMainForm.FormResize(Sender: TObject);
begin
  snapTopSplitterToMenu;
end;

procedure TMainForm.mnuGitBranchClick(Sender: TObject);
begin
  actProjGitBranchesUpd.Execute;
end;

procedure TMainForm.mnuItemAboutClick(Sender: TObject);
begin
  fInfoWidg.showWidget;
end;

procedure TMainForm.mnuItemCheckUpdClick(Sender: TObject);
var
  url: string;
begin
  url := checkForUpdate;
  if url.isEmpty then
    exit;
  if dlgYesNo('An new release is available, do you wish to visit the release page ?' +
    lineEnding + '(' + url +')') = mrYes then
      OpenURL(url);
end;

procedure TMainForm.mnuItemManualClick(Sender: TObject);
begin
  OpenURL('https://basile.b.gitlab.io/dexed/');
end;

destructor TMainForm.destroy;
begin
  SaveSettings;
  //
  fWidgList.Free;
  fProjMru.Free;
  fFileMru.Free;
  fPrjGrpMru.Free;
  FreeRunnableProc;
  //
  fOptionCategories.Free;
  EntitiesConnector.removeObserver(self);
  inherited;
  fLifeTimeStatusProvider.free;
end;

procedure TMainForm.UpdateDockCaption(Exclude: TControl = nil);
begin
  // otherwise dockmaster puts the widget list.
  Caption := 'dexed';
end;

procedure TMainForm.ApplicationProperties1Exception(Sender: TObject;E: Exception);
begin
  if fMesgWidg.isNotAssigned then
    dlgOkError(E.Message)
  else
    fMsgs.message(E.Message, nil, amcApp, amkErr);
end;

procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: boolean);
var
  i: Integer;
  f: string = '';
  p: string = '';
  g: string = #9'no';
  c: boolean = false;
  d: TDexedMemo = nil;
  b: TTaskDialogBaseButtonItem = nil;

const
  s: string = 'The following content is modified and changes will be lost'#10#10 +
              '- Modified files:'#10' %s '#10 +
              '- Modified projects:'#10' %s '#10 +
              '- Project group modified:'#10' %s';
begin
  canClose := false;

  if checkProjectLock(false) and
    (dlgOkCancel('A project is still being compiled, close anyway ?') <> mrOK) then
      exit;

  if fFreeProj.isAssigned and fFreeProj.modified then
  begin
    p += #9 + fFreeProj.filename + LineEnding;
    c := true;
  end;

  for i := 0 to fMultidoc.documentCount-1 do
  begin
    d := fMultidoc.getDocument(i);
    d.disableFileDateCheck := true;
    if d.modified or d.isTemporary then
    begin
      f += #9 + shortenPath(d.filename) + LineEnding;
      c := true;
    end;
  end;

  for i:= 0 to fProjectGroup.projectCount-1 do
  begin
    if not fProjectGroup.projectModified(i) then
      continue;
    p += #9 + shortenPath(fProjectGroup.getProject(i).filename) + LineEnding;
    c := true;
  end;

  if fProjectGroup.groupModified then
  begin
    g := #9'yes';
    c := true;
  end;

  if c then
  begin
    BringToFront;
    if p.isEmpty then
      p := '(no modified projects)'#10;
    if f.isEmpty then
      f := '(no modified files)'#10;

    with TTaskDialog.Create(nil) do
    try
      MainIcon := TTaskDialogIcon.tdiWarning;
      CommonButtons := [];
      Text := format(s, [f, p, g]);

      b := Buttons.Add;
      b.Caption := 'Quit';
      b.ModalResult := mrOK;

      b := Buttons.Add;
      b.Caption := 'Save and quit';
      b.ModalResult := mrAll;

      b := Buttons.Add;
      b.Caption := 'Do no quit';
      b.ModalResult := mrCancel;

      if Execute then
      begin
        if ModalResult = mrCancel then
        begin
          for i := 0 to fMultidoc.documentCount-1 do
          begin
            d := fMultidoc.getDocument(i);
            d.disableFileDateCheck := false;
          end;
          exit;
        end
        else if ModalResult = mrAll then
        begin
          for i := 0 to fMultidoc.documentCount-1 do
          begin
            d := fMultidoc.document[i];
            if d.modified and not d.isTemporary then
              d.save;
          end;
          if fProj.isAssigned and fProj.modified then
            fProj.saveToFile(fProj.filename);
          for i := 0 to fProjectGroup.projectCount-1 do
            if fProjectGroup.projectModified(i) then
              fProjectGroup.getProject(i).saveToFile(fProjectGroup.getProject(i).filename);
          if fProjectGroup.groupModified then
            fProjectGroup.saveGroup(fProjectGroup.groupFilename);
        end;
      end;
    finally
      free;
    end;
  end;
  fLifeTimeStatusProvider.lifetimeStatus:=lfsExiting;
  SaveLastDocsAndProj;
  CanClose:= true;
  fProjectGroup.closeGroup;
  if fFreeProj.isAssigned then
    fFreeProj.getProject.Free;
  for i:= fMultidoc.documentCount-1 downto 0 do
    fMultidoc.closeDocument(i, false);
end;

procedure TMainForm.updateDocumentBasedAction(sender: TObject);
begin
  TAction(sender).Enabled := fDoc.isAssigned;
end;

procedure TMainForm.updateProjectBasedAction(sender: TObject);
begin
  TAction(sender).Enabled := fProj.isAssigned {and not fProjActionsLock};
end;

procedure TMainForm.updateDocEditBasedAction(sender: TObject);
begin
  if fDoc.isAssigned and fDoc.Focused then
    TAction(sender).Enabled := true
  else
    TAction(sender).Enabled := false;
end;

procedure TMainForm.mruChange(Sender: TObject);
var
  srcLst: TMRUFileList;
  trgMnu: TMenuItem;
  itm: TMenuItem;
  fname: string;
  clickTrg: TNotifyEvent;
  i: integer;
  s: string;
begin
  srcLst := TMRUFileList(Sender);
  if srcLst.isNotAssigned then
    exit;
  trgMnu := TMenuItem(srcLst.objectTag);
  if trgMnu.isNotAssigned then
    exit;

  if fUpdateCount > 0 then
    exit;
  Inc(fUpdateCount);
  try
    if srcLst = fFileMru then
      clickTrg := @mruFileItemClick
    else if srcLst = fProjMru then
      clickTrg := @mruProjItemClick
    else if srcLst = fPrjGrpMru then
      clickTrg:= @mruProjGroupItemClick;

    trgMnu.Clear;

    for i := 0 to srcLst.Count-1 do
    begin
      s := srcLst.Strings[i];
      if srcLst = fFileMru then
        fname := s.extractFileName
      else
        fname := s.extractFileDir.extractFileName;

      itm := TMenuItem.Create(trgMnu);
      itm.Hint := s;
      itm.Caption := fname + ' - (' + itm.Hint + ')';
      itm.OnClick := clickTrg;
      trgMnu.Add(itm);
    end;

    trgMnu.AddSeparator;
    itm := TMenuItem.Create(trgMnu);
    itm.Caption := 'Clear all';
    itm.OnClick := @mruClearClick;
    itm.Tag := PtrInt(srcLst);
    itm.ImageIndex:=fCleanIconIndex;
    trgMnu.Add(itm);

    itm := TMenuItem.Create(trgMnu);
    itm.Caption := 'Remove invalid entries';
    itm.OnClick := @mruClearInvalidClick;
    itm.Tag := PtrInt(srcLst);
    itm.ImageIndex:=fCleanIconIndex;
    trgMnu.Add(itm);

  finally
    Dec(fUpdateCount);
  end;
end;

procedure TMainForm.mruClearClick(Sender: TObject);
var
  srcLst: TMRUFileList;
begin
  srcLst := TMRUFileList(TmenuItem(Sender).Tag);
  if srcLst.isAssigned then
    srcLst.Clear;
end;

procedure TMainForm.mruClearInvalidClick(Sender: TObject);
var
  srcLst: TMRUFileList;
  i: integer;
begin
  srcLst := TMRUFileList(TmenuItem(Sender).Tag);
  if srcLst.isNotAssigned then
    exit;
  srcLst.BeginUpdate;
  for i := srcLst.Count-1 downto 0 do
    if not srcLst.Strings[i].fileExists then
      srcLst.Delete(i);
  srcLst.EndUpdate;
end;
{$ENDREGION}

{$REGION IMultiDocMonitor ----------------------------------------------------}
procedure TMainForm.docNew(document: TDexedMemo);
begin
  fDoc := document;
end;

procedure TMainForm.docClosing(document: TDexedMemo);
begin
  if document <> fDoc then
    exit;
  fDoc := nil;
end;

procedure TMainForm.docFocused(document: TDexedMemo);
begin
  fDoc := document;
end;

procedure TMainForm.docChanged(document: TDexedMemo);
begin
  fDoc := document;
end;
{$ENDREGION}

{$REGION IProjectObserver ----------------------------------------------------}
procedure TMainForm.projNew(project: ICommonProject);
begin
  fProj := project;
  case fProj.getFormat of
    pfDEXED: fNativeProject := TNativeProject(fProj.getProject);
    pfDUB: fDubProject := TDubProject(fProj.getProject);
  end;
  if not fProj.inGroup then
    fFreeProj := project;
end;

procedure TMainForm.projChanged(project: ICommonProject);
begin
  showProjTitle;
end;

procedure TMainForm.projClosing(project: ICommonProject);
begin
  if project = fFreeProj then
    fFreeProj := nil;
  if fProj <> project then
    exit;
  fProj := nil;
  fDubProject := nil;
  fNativeProject := nil;
  showProjTitle;
end;

procedure TMainForm.projFocused(project: ICommonProject);
begin
  fProj := project;
  case fProj.getFormat of
    pfDEXED: fNativeProject := TNativeProject(fProj.getProject);
    pfDUB: fDubProject := TDubProject(fProj.getProject);
  end;
  if not fProj.inGroup then
    fFreeProj := project
  else if project = fFreeProj then
    fFreeProj := nil;

  if fProj.isAssigned and mnuGitBranch.Count.equals(0) then
    actProjGitBranchesUpdExecute(nil);

  showProjTitle;
end;

procedure TMainForm.projCompiling(project: ICommonProject);
begin
  fProjActionsLock := true;
  if fAppliOpts.showBuildDuration and not fIsCompilingGroup then
    fCompStart := GetTickCount64;
end;

procedure TMainForm.projCompiled(project: ICommonProject; success: boolean);
var
  runArgs: string = '';
  runprev: boolean = true;
  groupok: boolean = true;
  i: integer;
begin
  fProjActionsLock := false;
  if not fIsCompilingGroup then
  begin
    if fAppliOpts.showBuildDuration then
    begin
      fMsgs.message('Build duration: ' + formatTicksAsDuration(GetTickCount64 - fCompStart),
        project, amcProj, amkInf);
    end;
    if fRunProjAfterCompile and fProj.isAssigned then
    begin
      if not success then
        runprev := dlgYesNo('last build failed, continue and run ?') = mrYes;
      if runprev then
      begin
        if fRunProjAfterCompArg and
          not InputQuery('Execution arguments', '', runargs) then
            runargs := '';
        fProj.run(runargs);
      end;
    end;
    fRunProjAfterCompile := false;
    fRunProjAfterCompArg := false;
  end
  else
  begin
    fGroupCompilationCnt += 1;
    if (fGroupCompilationCnt = fProjectGroup.projectCount) then
    begin
      for i:= 0 to fProjectGroup.projectCount-1 do
        if not fProjectGroup.getProject(i).compiled then
      begin
        groupok := false;
        break;
      end;
      if not groupok then
        fMsgs.message('error, the project group is not fully compiled', nil, amcAll, amkErr)
      else
        fMsgs.message('the project group is successfully compiled', nil, amcAll, amkInf);
      if fAppliOpts.showBuildDuration then
      begin
        fMsgs.message('Group build duration: ' + formatTicksAsDuration(GetTickCount64 - fCompStart),
          nil, amcAll, amkInf);
      end;
      if fProjBeforeGroup.isAssigned then
        fProjBeforeGroup.activate;
      fProjBeforeGroup := nil;
      fIsCompilingGroup := false;
    end;
  end;
end;
{$ENDREGION}

{$REGION IEditableShortCut ---------------------------------------------------}
function TMainForm.scedCount: integer;
begin
  result := actions.ActionCount;
end;

function TMainForm.scedGetItem(const index: integer): TEditableShortcut;
var
  a: TCustomAction;
begin
  a                 := TCustomAction(Actions.Actions[index]);
  result.category   := a.Category;
  result.identifier := a.Caption;
  result.shortcut   := a.ShortCut;
end;

procedure TMainForm.scedSetItem(const index: integer; constref item: TEditableShortcut);
var
  a: TCustomAction;
begin
  a         := TCustomAction(Actions.Actions[index]);
  a.ShortCut:= item.shortcut;
end;
{$ENDREGION}

{$REGION IMainMenu -----------------------------------------------------------}
function TMainForm.singleServiceName: string;
begin
  exit('IMainMenu');
end;

function TMainForm.mnuAdd: TMenuItem;
begin
  result := TMenuItem.Create(nil);
  mainMenu.Items.Add(result);
  exit(result);
end;

procedure TMainForm.mnuDelete(value: TMenuItem);
var
  i: integer;
begin
  if value.isNotAssigned then
    exit;
  i := mainMenu.Items.IndexOf(value);
  if i <> -1 then
    mainMenu.Items.Delete(i);
end;
{$ENDREGION}

{$REGION file ------------------------------------------------------------------}
procedure TMainForm.actFileHtmlExportExecute(Sender: TObject);
var
  exp: TSynExporterHTML;
begin
  if fDoc.isNotAssigned then
    exit;
  exp := TSynExporterHTML.Create(nil);
  try
    with TSaveDialog.Create(nil) do
    try
      if Execute then
      begin
        filename := FileName.normalizePath;
        exp.Highlighter := fDoc.Highlighter;
        exp.Title := fDoc.fileName;
        exp.ExportAsText:=true;
        exp.ExportAll(fDoc.Lines);
        exp.SaveToFile(filename);
      end;
    finally
      Free;
    end;
  finally
    exp.Free;
  end;
end;

procedure TMainForm.newFile;
begin
  TDexedMemo.Create(nil);
end;

procedure TMainForm.openFile(const fname: string);
begin
  fMultidoc.openDocument(fname);
end;

procedure TMainForm.saveFile(document: TDexedMemo);
begin
  if (document.Highlighter = LfmSyn) or (document.Highlighter = JsSyn) then
    saveProjSource(document)
  else if document.fileName.fileExists then
    document.save;
end;

procedure TMainForm.mruFileItemClick(Sender: TObject);
begin
  openFile(TMenuItem(Sender).Hint);
end;

procedure TMainForm.actFileOpenExecute(Sender: TObject);
var
  fname: string;
begin
  with TOpenDialog.Create(nil) do
  try
    if fDoc.isAssigned and not fDoc.isTemporary and fDoc.fileName.fileExists then
      initialDir := fDoc.fileName.extractFileDir;
    options := options + [ofAllowMultiSelect];
    filter := DdiagFilter;
    if execute then
      for fname in files do
        openFile(fname.normalizePath);
  finally
    free;
  end;
end;

procedure TMainForm.actProjOpenContFoldExecute(Sender: TObject);
begin
  if fProj.isNotAssigned or not fProj.filename.fileExists then
    exit;
  getExplorer.browse(fProj.filename.extractFilePath);
  fExplWidg.showWidget;
end;

procedure TMainForm.actFileNewExecute(Sender: TObject);
begin
  newFile;
  fDoc.setFocus;
end;

procedure TMainForm.actFileNewRunExecute(Sender: TObject);
const
  body: array[boolean] of string =
  (
    LineEnding,
    '    // this file can be directly executed using menu file/compile & run' + LineEnding +
    '    // phobos and libman imports are allowed' + LineEnding +
    '    writeln("hello runnable module");' + LineEnding
  );
begin
  newFile;
  fDoc.Text :=
  'module runnable;' + LineEnding +
  LineEnding +
  'import std.stdio;' + LineEnding +
  LineEnding +
  'void main(string[] args)' + LineEnding +
  '{' + LineEnding +
      body[fFirstTimeRun] +
  '}';
  fDoc.setFocus;
end;

procedure TMainForm.actFileSaveAsExecute(Sender: TObject);
begin
  if fDoc.isNotAssigned then
    exit;
  with TSaveDialog.Create(nil) do
  try
    Filter := DdiagFilter;
    if not fDoc.isTemporary and fDoc.fileName.fileExists then
      InitialDir := fDoc.fileName.extractFileDir;
    if execute then
      fDoc.saveToFile(filename.normalizePath);
  finally
    free;
  end;
end;

procedure TMainForm.actFileSaveExecute(Sender: TObject);
var
  str: string;
begin
  if fDoc.isNotAssigned then
    exit;

  str := fDoc.fileName;
  if (str <> fDoc.tempFilename) and str.fileExists then
    saveFile(fDoc)
  else
    actFileSaveAs.Execute;
end;

procedure TMainForm.actFileAddToProjExecute(Sender: TObject);
begin
  if fDoc.isNotAssigned or fProj.isNotAssigned then
    exit;
  if fProj.filename = fDoc.fileName then
    exit;

  if fProj.getFormat = pfDEXED then
  begin
    if fDoc.fileName.fileExists and not fDoc.isTemporary then
      fNativeProject.addSource(fDoc.fileName)
    else dlgOkInfo('the file has not been added to the project because it does not exist');
  end else
    getMessageDisplay.message('use the DUB project editor to add a source to a DUB project',
      nil, amcApp, amkHint);
end;

procedure TMainForm.actFileCloseExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    getMultiDocHandler.closeDocument(fDoc);
end;

procedure TMainForm.actFileSaveAllExecute(Sender: TObject);
var
  i: Integer;
begin
  for i:= 0 to fMultidoc.documentCount-1 do
    saveFile(fMultidoc.document[i]);
end;

procedure TMainForm.FormDropFiles(Sender: TObject;const fnames: array of string);
var
  fname: string;
begin
  for fname in fnames do
  begin
    if isEditable(fname) then
      openFile(fname)
    else if isValidNativeProject(fname) or isValidDubProject(fname) then
    begin
      openProj(fname);
      break;
    end
    else openFile(fname);
  end;
end;

procedure TMainForm.actFileSaveCopyAsExecute(Sender: TObject);
var
  str: TStringList;
begin
  if fDoc.isNotAssigned then
    exit;
  with TSaveDialog.create(nil) do
  try
    if fDoc.isDSource then
      Filter := DdiagFilter;
    if fDoc.fileName.fileExists and not fDoc.isTemporary then
      InitialDir := fDoc.fileName.extractFileDir;
    if execute then
    begin
      str := TStringList.create;
      try
        str.assign(fDoc.Lines);
        str.saveToFile(FileName.normalizePath);
      finally
        str.free;
      end;
    end;
  finally
    free;
  end;
end;

{procedure TMainForm.ActionsExecute(AAction: TBasicAction; var Handled: Boolean);
var
  HintAction: TCustomHintAction absolute AAction;
begin
  if (AAction is TCustomHintAction) and
    string(HintAction.Hint).StartsWith(<magic string>) then
  begin
    // here attempt to show TMenuItem.Hint can be detected
  end;
end;}

procedure TMainForm.actLayoutResetExecute(Sender: TObject);
begin
  InitDocking(true);
end;
{$ENDREGION}

{$REGION edit ------------------------------------------------------------------}
procedure TMainForm.actEdCopyExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    fDoc.CopyToClipboard;
end;

procedure TMainForm.actEdCutExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    fDoc.CutToClipboard;
end;

procedure TMainForm.actEdPasteExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    fDoc.PasteFromClipboard;
end;

procedure TMainForm.actEdUndoExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    fDoc.Undo;
end;

procedure TMainForm.actEdRedoExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    fDoc.Redo;
end;

procedure TMainForm.actEdMacPlayExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    fEditWidg.macRecorder.PlaybackMacro(fDoc);
end;

procedure TMainForm.actEdMacStartStopExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
  begin
    if fEditWidg.macRecorder.State = msRecording then
      fEditWidg.macRecorder.Stop
    else fEditWidg.macRecorder.RecordMacro(fDoc);
  end;
end;

procedure TMainForm.actEdIndentExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    fDoc.ExecuteCommand(ecBlockIndent, '', nil);
end;

procedure TMainForm.actEdUnIndentExecute(Sender: TObject);
begin
  if fDoc.isAssigned then
    fDoc.ExecuteCommand(ecBlockUnIndent, '', nil);
end;

procedure TMainForm.actEdFindExecute(Sender: TObject);
var
  str: string;
begin
  if fDoc.isNotAssigned then
      exit;
  fFindWidg.showWidget;

  if fDoc.SelAvail then
    str := fDoc.SelText
  else
    str := fDoc.Identifier;
  ffindwidg.cbToFind.Text := str;
  ffindwidg.cbToFindChange(nil);
  ffindwidg.cbToFind.SetFocus;
end;

procedure TMainForm.actEdFindNextExecute(Sender: TObject);
begin
  ffindwidg.actFindNextExecute(nil);
end;
{$ENDREGION}

{$REGION run -------------------------------------------------------------------}
function TMainForm.runnableExename: string;
var
  of_yes: string;
  of_no: string;
begin
  result := '';
  if fDoc.isNotAssigned then
    exit;

  of_no := fDoc.fileName.stripFileExt + exeExt;
  of_yes:= fRunnablesOptions.outputFolder;

  if not FilenameIsAbsolute(of_yes) then
    of_yes := fDoc.fileName.extractFilePath + of_yes +
    fDoc.fileName.extractFileName.stripFileExt + exeExt
  else
    of_yes := fRunnablesOptions.outputFolder +
    fDoc.fileName.extractFileName.stripFileExt + exeExt;
  result := of_no;

  if fRunnablesOptions.outputFolderConditions <> [] then
  begin
    if ifNotSaved in fRunnablesOptions.outputFolderConditions then
    begin
      if fDoc.isTemporary then
        result := of_yes;
    end
    else if fProj.isAssigned then
    begin
      if ifInProject in fRunnablesOptions.outputFolderConditions then
      begin
        if fProj.isSource(fDoc.fileName) then
          result := of_yes;
      end
      else if ifSaved in fRunnablesOptions.outputFolderConditions then
      begin
        if not fProj.isSource(fDoc.fileName) and not fDoc.isTemporary then
          result := of_yes;
      end;
    end
  end;

end;

procedure TMainForm.asyncprocOutput(sender: TObject);
var
  proc: TDexedProcess;
  lst: TStringList;
  str: string;
begin
  proc := TDexedProcess(sender);
  lst := TStringList.Create;
  try
    proc.getFullLines(lst);
    fMsgs.beginMessageCall();
    if proc = fRunProc then
      for str in lst do
        fMsgs.message(str, fDoc, amcEdit, amkBub)
    else // dmd used to compile runnable
      for str in lst do
        fMsgs.message(str, fDoc, amcEdit, amkAuto);
    fMsgs.endMessageCall();
  finally
    lst.Free;
  end;
end;

procedure TMainForm.asyncprocTerminate(sender: TObject);
var
  proc: TDexedProcess;
  inph: TObject;
begin
  proc := TDexedProcess(sender);
  asyncprocOutput(sender);
  inph := EntitiesConnector.getSingleService('IProcInputHandler');
  if inph.isAssigned then
    (inph as IProcInputHandler).removeProcess(proc);
  if not proc.ExitStatus.equals(0) then
  begin
    fMsgs.message(format('error: the process (%s) has returned the status %s',
      [proc.Executable, prettyReturnStatus(proc)]), fDoc, amcEdit, amkErr);
    if proc.autoKilled then
      fMsgs.message(format('the process was autokilled because the size of its output exceeded %d',
        [proc.autoKillProcThreshold]), nil, amcEdit, amkWarn);
  end;
end;

procedure TMainForm.actSetRunnableSwExecute(Sender: TObject);
var
  form: TForm;
  memo: TMemo;
begin
  if fRunnablesOptions.fStaticSwitches.Count.equals(0) then
    fRunnablesOptions.setDefaultSwitches;
  form := TForm.Create(nil);
  form.BorderIcons:= [biSystemMenu];
  memo := TMemo.Create(form);
  memo.Align := alClient;
  memo.BorderSpacing.Around:=4;
  memo.Lines.Assign(fRunnablesOptions.staticSwitches);
  memo.Parent := form;
  form.ShowModal;

  fRunnablesOptions.staticSwitches.Assign(memo.Lines);
  fRunnablesOptions.sanitizeSwitches;

  form.Free;
end;

procedure TMainForm.ApplicationProperties1Activate(Sender: TObject);
begin
  if fDoc.isAssigned then
    fDoc.checkFileDate;
end;

function TMainForm.compileRunnable(unittest: boolean = false): boolean;
var
  i: integer;
  fname: string;
  dmdproc: TDexedProcess;
  lst: TStringList = nil;
  srt: TStringList;
  firstLineFlags: string = '';
  asObj: boolean = false;
  hasMain: THasMain;
  rng: TStringRange = (ptr:nil; pos:0; len: 0);
  s: string;
begin

  if fAppliOpts.showBuildDuration then
    fCompStart := GetTickCount64;

  result := false;
  fMsgs.clearByData(fDoc);
  FreeRunnableProc;
  if fDoc.isNotAssigned or fDoc.Lines.Count.equals(0) then
    exit;

  firstlineFlags := fDoc.Lines[0];
  rng.init(firstLineFlags);
  if rng.startsWith('#!') then
  begin
    rng.popFrontN(2)^
      .popWhile([' ', #9])^
      .popUntil([' ', #9, ':'])^
      .popWhile([' ', #9, ':']);
    firstlineFlags := rng.takeUntil(#0).yield;
    firstlineFlags := fSymStringExpander.expand(firstlineFlags);
    lst := TStringList.Create;
    CommandToList(firstlineFlags, lst);
    for i:= lst.Count-1 downto 0 do
    begin
      s := lst[i];
      if (s.length > 2) and (s[1..3] = '-of') then
      begin
        lst.Delete(i);
        fMsgs.message('the option "-of" is ignored when compiling a runnable modules',
          fDoc, amcEdit, amkWarn);
      end
      else if s = '-c' then
      begin
        if not unittest then
          asObj:=true
        else
        begin
          lst.Delete(i);
          fMsgs.message('the option "-c" is ignored when an individual module is tested',
            fDoc, amcEdit, amkWarn);
        end;
      end
      else if s = '-run' then
        lst.Delete(i);
    end;
  end;

  dmdproc := TDexedProcess.Create(nil);
  try
    fMsgs.message('compiling ' + shortenPath(fDoc.fileName, 25), fDoc, amcEdit, amkInf);
    fMsgs.message(usingCompilerInfo(fRunnablesOptions.compiler, true), fDoc, amcEdit, amkInf);
    if fDoc.fileName.fileExists then
      fDoc.save
    else
      fDoc.saveTempFile;
    fname := runnableExename.stripFileExt;

    if fRunnablesOptions.staticSwitches.Count.equals(0) then
      fRunnablesOptions.setDefaultSwitches;
    {$IFDEF RELEASE}
    dmdProc.ShowWindow := swoHIDE;
    {$ENDIF}
  	dmdproc.OnReadData := @asyncprocOutput;
  	dmdproc.OnTerminate:= @asyncprocTerminate;
    dmdproc.Options := [poUsePipes, poStderrToOutPut];
    dmdproc.CurrentDirectory:=fDoc.fileName.extractFileDir;
    dmdProc.Executable:= fCompilerSelector.getCompilerPath(fRunnablesOptions.compiler, true);
    if not dmdProc.Executable.fileExists then
    begin
      fMsgs.message(format('error, the compiler path for `%s` does not seem valid',
        [DCompiler2String[fRunnablesOptions.compiler]]), fDoc, amcEdit, amkErr);
      fMsgs.message('check menu `Options`, `Compilers Paths`', fDoc, amcEdit, amkHint);
      exit;
    end;
    dmdproc.Parameters.Add(fDoc.fileName);
    if not asObj then
      dmdproc.Parameters.Add('-of' + fname + exeExt)
    else
      dmdproc.Parameters.Add('-of' + fname + objExt);
    dmdproc.Parameters.Add('-J' + fDoc.fileName.extractFilePath);
    dmdproc.Parameters.AddStrings(fRunnablesOptions.staticSwitches);
    if lst.isAssigned and not lst.Count.equals(0) then
      dmdproc.Parameters.AddStrings(lst);
    {$ifdef WINDOWS}
    {$ifdef CPUX86_64}
    if lst.isAssigned and (lst.IndexOf('-m32') <> -1) then
    begin
      i := dmdproc.Parameters.IndexOf('-m64');
      if i <> -1 then
        dmdproc.Parameters.Delete(i);
    end;
    {$endif}
    {$endif}
    if fRunnablesOptions.detectMain then
    begin
      hasMain := fDoc.implementMain;
      case hasMain of
        mainNo:
          dmdproc.Parameters.Add('-main');
        mainDefaultBehavior:
          if unittest then
            dmdproc.Parameters.Add('-main');
      end;
    end;
    dmdproc.Parameters.Add('-version=single_module');
    if unittest then
    begin
      if not fRunnablesOptions.detectMain then
        dmdproc.Parameters.Add('-main');
      dmdproc.Parameters.Add('-unittest');
      if fCovModUt then
        dmdproc.Parameters.Add('-cov');
      // NOTE: see #258, allows to test easily a module when hacking phobos.
      dmdproc.Parameters.AddStrings(['-version=StdUnittest', '-version=test_single_module']);
    end
    else
      dmdproc.Parameters.AddStrings(['-version=runnable_module', '-version=run_single_module']);

    if fRunnablesOptions.detectLibraries then
      LibMan.getLibsForSource(fDoc.Lines, dmdproc.Parameters, dmdproc.Parameters)
    else
    begin
      srt := TStringList.Create;
      try
        srt.Sorted:=true;
        //NOTE: when not sorted linking can fail. This is a recent regression (~2.078)
        //when detectLibraries is true, sorting is automatic *.a, -Ipath, *.a, -Ipath etc
        srt.Duplicates := TDuplicates.dupIgnore;
        LibMan.getLibFiles(nil, srt);
        LibMan.getLibSourcePath(nil, srt);
        dmdproc.Parameters.AddStrings(srt);
      finally
        srt.Free;
      end;
    end;
    deleteDups(dmdproc.Parameters);
    dmdproc.Execute;
    while dmdproc.Running do
      application.ProcessMessages;

    // The timer in TDexedProcess still does not fix
    // the "onTerminated never called bug"
    if not dmdproc.doneTerminated then
    begin
      dmdProc.fillOutputStack;
      asyncprocTerminate(dmdproc);
    end;

    if not asObj then
      sysutils.DeleteFile(fname + objExt);
    if dmdProc.ExitStatus.equals(0) then
    begin
      result := true;
      fMsgs.message(shortenPath(fDoc.fileName, 25) + ' successfully compiled',
        fDoc, amcEdit, amkInf);
    end
    else begin
      //fMsgs.message(format('error: the process (%s) has returned the status %s',
      //  [dmdproc.Executable, prettyReturnStatus(dmdproc)]), fDoc, amcEdit, amkErr);
      fMsgs.message(shortenPath(fDoc.fileName, 25) + ' has not been compiled',
        fDoc, amcEdit, amkErr);
    end;
    if fAppliOpts.showBuildDuration then
    begin
      fMsgs.message('Runnable build duration: ' + formatTicksAsDuration(GetTickCount64 - fCompStart),
        fDoc, amcEdit, amkInf);
    end;

  finally
    dmdproc.Free;
    if lst.isAssigned then
      lst.Free;
  end;
end;

procedure TMainForm.executeRunnable(unittest: boolean = false; redirect: boolean = true;
	const runArgs: string = '');
var
  lst: TStringList;
  fname: string;
begin
  if fDoc.isNotAssigned then
    exit;
  fname := runnableExename;
  if not fname.fileExists then
    exit;

  fRunProc := TDexedProcess.Create(nil);
  if redirect then
  begin
  	fRunProc.Options := [poStderrToOutPut, poUsePipes];
  	fRunProc.ShowWindow := swoHIDE;
  	fRunProc.OnReadData := @asyncprocOutput;
  	fRunProc.OnTerminate:= @asyncprocTerminate;
  end
  else
  begin
    {$IFNDEF WINDOWS}
    fRunProc.Options := fRunProc.Options + [poNewConsole];
    {$ENDIF}
    fRunProc.XTermProgram:=consoleProgram;
  end;
  lst := TStringList.Create;
  try
    if runArgs.isNotEmpty then
    begin
      CommandToList(fSymStringExpander.expand(runArgs), lst);
      fRunProc.Parameters.AddStrings(lst);
    end;
    fRunProc.Executable := fname;
    fRunProc.CurrentDirectory := fRunProc.Executable.extractFileDir;
    if unittest and fCovModUt then
      fRunProc.OnTerminate:=@unittestDone;
    if redirect then
      getprocInputHandler.addProcess(fRunProc);
    fRunProc.Execute;
  finally
    lst.Free;
  end;
end;

procedure TMainForm.unittestDone(Sender: TObject);
var
  fullcov: boolean;
  fname, covname: string;
  lst: TStringList;
  i: integer;
  r: string = '';
const
  ic : array[boolean] of TAppMessageKind = (amkWarn, amkInf);
begin
  asyncprocTerminate(sender);
  if fCovModUt and fRunProc.isAssigned and fRunProc.ExitStatus.equals(0) then
  begin
    fname   := fDoc.fileName.stripFileExt;
    fullcov := true;
    covname := ReplaceStr(fname + '.lst', DirectorySeparator, '-');
    {$IFDEF WINDOWS}
    covname := ReplaceStr(covname, DriveSeparator, '-');
    {$ENDIF}
    if not covname.fileExists then
    begin
      r := fRunProc.Executable.extractFilePath;
      covname := r + covname;
    end;
    if covname.fileExists then
    begin
      lst := TStringList.Create;
      try
        lst.LoadFromFile(covname);
        for i := 0 to lst.Count-1 do
          if lst[i][1..7] = '0000000' then
        begin
          fMsgs.message(format('%s(%d): %s', [fDoc.fileName, i+1,
            'not covered by the unittests']), fDoc, amcEdit, amkWarn);
          fullcov := false;
        end;
        sysutils.DeleteFile(covname);
        sysutils.DeleteFile(r + '__main.lst');
        fMsgs.message(lst[lst.Count-1], fDoc, amcEdit, ic[fullcov]);
      finally
        lst.free;
      end;
    end else
      fMsgs.message('the coverage file cannot be found', fDoc, amcEdit, amkWarn);
  end;
end;

procedure TMainForm.actFileUnittestExecute(Sender: TObject);
begin
  if fDoc.isNotAssigned then
    exit;
  if compileRunnable(true) then
    executeRunnable(true, true);
end;

procedure TMainForm.actFileCompAndRunExecute(Sender: TObject);
begin
  if fDoc.isNotAssigned then
    exit;
  if compileRunnable(false) then
    executeRunnable(false, true);
end;

procedure TMainForm.actFileCompileAndRunOutExecute(Sender: TObject);
begin
  if fDoc.isNotAssigned then
    exit;
  if compileRunnable(false) then
    executeRunnable(false, false);
end;

procedure TMainForm.actFileCompAndRunWithArgsExecute(Sender: TObject);
var
  runargs: string = '';
begin
  if fDoc.isNotAssigned then
    exit;
  if compileRunnable(false) and InputQuery('Execution arguments', '', runargs) then
    executeRunnable(false, true, runargs);
end;

procedure TMainForm.actFileCompileExecute(Sender: TObject);
begin
  compileRunnable(false);
end;

procedure TMainForm.actFileCloseAllOthersExecute(Sender: TObject);
var
  i: integer;
  d: TDexedMemo;
  c: TDexedMemo;
begin
  if fDoc.isNotAssigned then
    exit;
  c := fDoc;
  for i := fMultidoc.documentCount-1 downto 0 do
  begin
    d := fMultidoc.document[i];
    if d = c then
      fMultidoc.closeDocument(d);
  end;
end;

procedure TMainForm.actFileCloseAllExecute(Sender: TObject);
var
  i: integer;
begin
  if fDoc.isNotAssigned then
    exit;
  for i := fMultidoc.documentCount-1 downto 0 do
    fMultidoc.closeDocument(i);
end;

procedure TMainForm.actEdFormatExecute(Sender: TObject);
begin
  if fDoc.isNotAssigned then
    exit;
  getCodeFormatting.formatCurrent();
end;

procedure TMainForm.actFileDscannerExecute(Sender: TObject);
var
  lst: TStringList;
  prc: TProcess;
  pth: string;
  msg: string;
begin
  if fDoc.isNotAssigned then
    exit;
  if fDoc.isTemporary and fDoc.modified then
    fDoc.saveTempFile;
  pth := exeFullName('dscanner' + exeExt);
  if not pth.fileExists then
    exit;
  prc := TProcess.Create(nil);
  lst := TStringList.Create;
  try
    prc.Executable:=pth;
    prc.Options := [poUsePipes, poStderrToOutPut {$IFDEF WINDOWS}, poNewConsole{$ENDIF}];
    prc.ShowWindow:= swoHIDE;
    prc.Parameters.AddStrings([fDoc.fileName, '-S']);
    if not fDscanUnittests then
      prc.Parameters.Add('--skipTests');
    prc.Execute;
    processOutputToStrings(prc, lst);
    while prc.Running do;
    for msg in lst do
      fMsgs.message(msg, fDoc, amcEdit, amkWarn);
  finally
    prc.Free;
    lst.Free;
  end;
end;

procedure TMainForm.actFileMetricsHalsteadExecute(Sender: TObject);
begin
  if fDoc.isNotAssigned or not fDoc.isDSource then
    exit;
  metrics.measure(fDoc);
end;

procedure TMainForm.actFileNewClipExecute(Sender: TObject);
begin
  newFile;
  fDoc.setFocus;
  fDoc.PasteFromClipboard;
end;

procedure TMainForm.actFileNewDubScriptExecute(Sender: TObject);
begin
  newFile;
  fDoc.Text :=
  '/+ dub.sdl:' + LineEnding +
  '   name "dub_script" +/' + LineEnding +
  'module dub_script;' + LineEnding +
  LineEnding +
  'import std.stdio;' + LineEnding +
  LineEnding +
  'void main(string[] args)' + LineEnding +
  '{' + LineEnding + '}';
  fDoc.setFocus;
end;

procedure TMainForm.actFileRunDubExecute(Sender: TObject);
begin
  dubFile(false);
end;

procedure TMainForm.actFileRunDubOutExecute(Sender: TObject);
begin
  dubFile(true);
end;

procedure TMainForm.dubFile(outside: boolean);
var
  d: string;
begin
  if fDoc.isNotAssigned then
    exit;
  FreeRunnableProc;
  fMsgs.clearByData(fDoc);
  fRunProc := TDexedProcess.Create(nil);
  if fDoc.fileName.fileExists then
    fDoc.save
  else
    fDoc.saveTempFile;
  fRunProc.Executable:= exeFullName('dub' + exeExt);
  fRunProc.Parameters.Add('--single');
  if not outside then
  begin
	  fRunProc.Options := [poStderrToOutPut, poUsePipes];
	  fRunProc.ShowWindow := swoHIDE;
	  fRunProc.OnReadData := @asyncprocOutput;
	  fRunProc.OnTerminate:= @asyncprocTerminate;
    getprocInputHandler.addProcess(fRunProc);
  end
  else
  begin
    {$IFNDEF WINDOWS}
    fRunProc.Options := fRunProc.Options + [poNewConsole];
    {$ENDIF}
    fRunProc.XTermProgram:=consoleProgram;
  end;
  d := fCompilerSelector.getCompilerPath(fRunnablesOptions.compiler, true);
  if not d.fileExists then
  begin
    fMsgs.message(format('error, the compiler path for `%s` does not seem valid',
      [DCompiler2String[fRunnablesOptions.compiler]]), fDoc, amcEdit, amkErr);
    fMsgs.message('check menu `Options`, `Compilers Paths`', fDoc, amcEdit, amkHint);
    exit;
  end;
  fRunProc.Parameters.AddStrings(['--compiler=' + d, fDoc.fileName]);
  fRunProc.execute;
end;

procedure TMainForm.runFile(outside: boolean);
var
  fname: string;
  older: boolean = false;
  exist: boolean = false;
const
  messg1: string = 'Either the runnable does not exist or it is older than its source.' +
    LineEnding +  'Do you wish to recompile it ?';
  messg2: string = 'The binary produced for a runnable that is not explicitly saved ' +
    'must be recompiled after each execution.' + LineEnding +  'Do you wish to recompile it now ?';
begin
  if fDoc.isNotAssigned then
    exit;
  FreeRunnableProc;
  fname := runnableExename;
  if fname.fileExists then
  begin
    exist := true;
    older := FileAge(fname) < FileAge(fDoc.fileName);
  end;
  if (not exist) or (older) then
  begin
    if fDoc.isTemporary and (dlgYesNo(messg2) = mrYes) then
      compileRunnable
    else if dlgYesNo(messg1) = mrYes then
      compileRunnable
    else if not exist then
      exit;
  end;
  if fname.fileExists then
    executeRunnable(false, not outside);
end;

procedure TMainForm.actFileRunExecute(Sender: TObject);
begin
  runFile(false);
end;

procedure TMainForm.actFileRunOutExecute(Sender: TObject);
begin
  runFile(true);
end;

procedure TMainForm.actFileOpenContFoldExecute(Sender: TObject);
begin
  if fDoc.isNotAssigned or not fDoc.fileName.fileExists then
    exit;
  getExplorer.browse(fDoc.fileName.extractFilePath);
  fExplWidg.showWidget;
end;

procedure TMainForm.actProjCompileExecute(Sender: TObject);
begin
  if fAppliOpts.autoSaveProjectFiles then
    saveModifiedProjectFiles(fProj);
  fProj.compile;
end;

procedure TMainForm.actProjCompileAndRunExecute(Sender: TObject);
begin
  fRunProjAfterCompile := true;
  if fAppliOpts.autoSaveProjectFiles then
    saveModifiedProjectFiles(fProj);
  fProj.compile;
end;

procedure TMainForm.actProjCompAndRunWithArgsExecute(Sender: TObject);
begin
  fRunProjAfterCompile := true;
  fRunProjAfterCompArg := true;
  if fAppliOpts.autoSaveProjectFiles then
    saveModifiedProjectFiles(fProj);
  fProj.compile;
end;

procedure TMainForm.actProjRunExecute(Sender: TObject);
begin
  if fProj.binaryKind <> executable then
    dlgOkInfo('Non executable projects cant be run')
  else
  begin
    if not fProj.targetUpToDate and
      (dlgYesNo('The project output is not up-to-date, rebuild ?') = mrYes) then
      begin
        if fAppliOpts.autoSaveProjectFiles then
          saveModifiedProjectFiles(fProj);
        fProj.compile;
      end;
    if fProj.outputFilename.fileExists or (fProj.getFormat = pfDUB) then
      fProj.run;
  end;
end;

procedure TMainForm.actProjRunWithArgsExecute(Sender: TObject);
var
  runargs: string = '';
begin
  if InputQuery('Execution arguments', '', runargs) then
    fProj.run(runargs);
end;
{$ENDREGION}

{$REGION view ------------------------------------------------------------------}
procedure TMainForm.updateWidgetMenuEntry(sender: TObject);
var
  widg: TDexedWidget;
  act: TAction;
begin
  if sender.isNotAssigned then
    exit;
  act := TAction(sender);
  if act.Tag.equals(0) then
    exit;

  widg := TDexedWidget(act.Tag);
  if widg.isDockable then
  begin
    if DockMaster.GetAnchorSite(widg).GetTopParent = DockMaster.GetAnchorSite(widg) then
      act.Enabled := true
    else
      act.Enabled := not widg.Parent.IsVisible
  end
  else act.Enabled := not widg.IsVisible;
end;

procedure TMainForm.widgetShowFromAction(sender: TObject);
var
  widg: TDexedWidget;
begin
  widg := TDexedWidget( TComponent(sender).tag );
  if widg.isAssigned then
    widg.showWidget;
end;

procedure TMainForm.layoutLoadFromFile(const fname: string);
var
  xcfg: TXMLConfigStorage;
begin
  if not fname.fileExists then
    exit;
  xcfg := TXMLConfigStorage.Create(fname, true);
  try
    DockMaster.RestoreLayouts.Clear;
    DockMaster.LoadLayoutFromConfig(xcfg, false);
  finally
    xcfg.Free;
  end;
end;

procedure TMainForm.layoutSaveToFile(const fname: string);
var
  x: TXMLConfigStorage;
  i: integer;
  w: TDexedWidget;
  h: TAnchorDockHostSite;
begin
  DockMaster.RestoreLayouts.Clear;
  for i:= 0 to fWidgList.Count-1 do
  begin
    w := fWidgList.widget[i];
    if not w.isDockable then
      continue;
    h := DockMaster.GetAnchorSite(w);
    if h.isAssigned and ((h.WindowState = wsMinimized) or (not h.HasParent)) then
      h.Close;
  end;

  forceDirectory(fname.extractFilePath);
  x := TXMLConfigStorage.Create(fname, false);
  try
    DockMaster.SaveLayoutToConfig(x);
    x.WriteToDisk;
  finally
    x.Free;
  end;
end;

procedure TMainForm.layoutUpdateMenu;
var
  lst: TStringList;
  itm: TMenuItem;
  i: integer;
begin
  itm := TMenuItem.Create(self);
  itm.Action := actLayoutReset;
  mnuLayout.Add(itm);

  itm := TMenuItem.Create(self);
  itm.Action := actLayoutSave;
  mnuLayout.Add(itm);
  mnuLayout.AddSeparator;

  lst := TStringList.Create;
  try
    listFiles(lst, getDocPath + 'layouts' + DirectorySeparator);
    for i := 0 to lst.Count-1 do
    begin
      itm := TMenuItem.Create(self);
      itm.Caption := lst[i].extractFileName;
      itm.Caption := stripFileExt(itm.Caption);
      itm.OnClick := @layoutMnuItemClick;
      itm.ImageIndex := 32;
      mnuLayout.Add(itm);
    end;
  finally
    lst.Free;
  end;
end;

procedure TMainForm.layoutMnuItemClick(sender: TObject);
begin
  layoutLoadFromFile(getDocPath + 'layouts' + DirectorySeparator +
    TMenuItem(sender).Caption + '.xml');
end;

procedure TMainForm.actLayoutSaveExecute(Sender: TObject);
var
  fname: string = '';
begin
  if not InputQuery('New layout name', '', fname) then
    exit;

  fname := fname.extractFileName;
  if fname.extractFileExt <> '.xml' then
    fname += '.xml';

  layoutSaveToFile(getDocPath + 'layouts' + DirectorySeparator + fname);
  layoutUpdateMenu;
end;

procedure TMainForm.updateFloatingWidgetOnTop(onTop: boolean);
var
  widg: TDexedWidget;
const
  fstyle: array[boolean] of TFormStyle = (fsNormal, fsStayOnTop);
begin
  for widg in fWidgList do if widg.Parent.isAssigned and
    widg.Parent.Parent.isNotAssigned and widg.isDockable then
      TForm(widg.Parent).FormStyle := fstyle[onTop];
end;

procedure TMainForm.snapTopSplitterToMenu;
var
  topSplt: TAnchorDockSplitter;
  topHost: TAnchorDockHostSite = nil;
  topSite: TControl;
  edtSite: TControl;
begin
  if not fDockingIsInitialized then
    exit;
  edtSite := DockMaster.GetSite(fEditWidg);
  if edtSite.isNotAssigned then
    exit;
  if GetDockSplitterOrParent(edtSite, akTop, topSite) then
  begin
    if topSite is TAnchorDockHostSite then
      topHost := TAnchorDockHostSite(topSite);
    if topHost.isAssigned and topHost.BoundSplitter.isAssigned and
      (topHost.BoundSplitter.Top > 0) then
    begin
      topHost.BoundSplitter.OnCanOffset:=nil;
      topHost.BoundSplitter.MoveSplitter(-topHost.BoundSplitter.Top);
      topHost.BoundSplitter.OnCanOffset:= @LockTopWindow;
    end;
  end else if GetDockSplitter(DockMaster.GetSite(fEditWidg), akTop, topSplt) and
    (topSplt.top > 0) then
  begin
    topSplt.OnCanOffset := nil;
    topSplt.MoveSplitter(-topSplt.top);
    topSplt.OnCanOffset:= @LockTopWindow;
  end;
end;
{$ENDREGION}

{$REGION project ---------------------------------------------------------------}
function  TMainForm.checkProjectLock(message: boolean = true): boolean;
begin
  result := false;
  if not fProjActionsLock then
    exit;
  result := true;
  if message then
    dlgOkInfo('This action is disabled while a project compiles',
      'Project lock warning');
end;

procedure TMainForm.showProjTitle;
begin
  if fProj.isAssigned and fProj.filename.fileExists then
    caption := format('dexed - %s', [shortenPath(fProj.filename, 30)])
  else
    caption := 'dexed';
end;

procedure TMainForm.saveProjSource(const document: TDexedMemo);
var
  fname: string;
begin
  if fProj.isNotAssigned or checkProjectLock or (fProj.filename <> document.fileName) then
      exit;

  fname := fProj.filename;
  document.saveToFile(fname);
  fProj.reload;
end;

function TMainForm.closeProj: boolean;
begin
  if fProj.isNotAssigned then
    exit(true);

  result := false;
  if fProj = fFreeProj then
  begin
    if checkProjectLock then
      exit;
    fProj.getProject.Free;
    fFreeProj := nil;
  end;
  fProj := nil;
  fNativeProject := nil;
  fDubProject := nil;
  showProjTitle;
  result := true;
end;

procedure TMainForm.actProjNewDialogExecute(Sender: TObject);
var
  r: TModalResult;
begin
  if fProj.isAssigned and not fProj.inGroup and fProj.modified and
    (dlgFileChangeClose(fProj.filename, UnsavedProj) = mrCancel) then
      exit;
  if not closeProj then
    exit;
  with TCeNewDubProject.create(nil) do
  try
    r := ShowModal();
    if r = mrOk then
      openProj(u_newdubproj.createdNewProject);
  finally
    free;
  end;
end;

procedure TMainForm.actProjNewDubJsonExecute(Sender: TObject);
begin
  if fProj.isAssigned and not fProj.inGroup and fProj.modified and
    (dlgFileChangeClose(fProj.filename, UnsavedProj) = mrCancel) then
      exit;
  if not closeProj then
    exit;
  newDubProj;
end;

procedure TMainForm.actProjNewNativeExecute(Sender: TObject);
begin
  if fProj.isAssigned and not fProj.inGroup and fProj.modified and
    (dlgFileChangeClose(fProj.filename, UnsavedProj) = mrCancel) then
      exit;
  if not closeProj then
    exit;
  newNativeProj;
end;

procedure TMainForm.newNativeProj;
begin
  fNativeProject := TNativeProject.Create(nil);
  fNativeProject.Name := 'CurrentProject';
  fProj := fNativeProject as ICommonProject;
  showProjTitle;
end;

procedure TMainForm.newDubProj;
begin
  fDubProject := TDubProject.create(nil);
  fProj := fDubProject as ICommonProject;
  showProjTitle;
end;

procedure TMainForm.saveProj;
begin
  fProj.saveToFile(fProj.filename);
end;

procedure TMainForm.saveProjAs(const fname: string);
begin
  fProj.saveToFile(fname);
  showProjTitle;
end;

procedure TMainForm.openProj(const fname: string);
var
  ext: string;
begin
  if not closeProj then
    exit;
  ext := fname.extractFileExt.upperCase;
  if (ext = '.JSON') or (ext = '.SDL') then
    newDubProj
  else if ext = '.DPRJ' then
    newNativeProj
  else
  begin
    dlgOkError('Unknown project extension : ' + ext);
    exit;
  end;

  fProj.loadFromFile(fname);
  showProjTitle;
  fProj.activate;
end;

procedure TMainForm.mruProjItemClick(Sender: TObject);
begin
  if checkProjectLock then
    exit;
  if fProj.isAssigned and not fProj.inGroup and fProj.modified and
    (dlgFileChangeClose(fProj.filename, UnsavedProj) = mrCancel) then
      exit;
  openProj(TMenuItem(Sender).Hint);
end;

procedure TMainForm.mruProjGroupItemClick(Sender: TObject);
begin
  if checkProjectLock then
    exit;
  if fProjectGroup.groupModified and (dlgFileChangeClose(
    fProjectGroup.groupFilename, UnsavedPGrp) = mrCancel) then
      exit;
  fProjectGroup.closeGroup;
  fProjectGroup.openGroup(TMenuItem(Sender).Hint);
  if fProj.isNotAssigned and (fProjectGroup.getProjectIndex < fProjectGroup.projectCount) then
    fProjectGroup.getProject(fProjectGroup.getProjectIndex).activate();
end;

procedure TMainForm.actProjCloseExecute(Sender: TObject);
begin
  if fProj.isAssigned and not fProj.inGroup and fProj.modified and
    (dlgFileChangeClose(fProj.filename, UnsavedProj) = mrCancel) then
      exit;
  closeProj;
end;

procedure TMainForm.actProjSaveAsExecute(Sender: TObject);
begin
  if checkProjectLock then
    exit;
  if (fProj.getFormat = pfDUB) and TDubProject(fProj.getProject).isSDL then
  begin
    fMsgs.message(DubSdlWarning, fProj, amcProj, amkWarn);
    exit;
  end;
  with TSaveDialog.Create(nil) do
  try
    Filter := 'DUB json|*.json|DUB sdl|*.sdl|Dexed project|*.dprj';
    if fProj.filename.fileExists then
      InitialDir := fProj.filename.extractFileDir;
    if execute then
      saveProjAs(filename.normalizePath);
  finally
    Free;
  end;
end;

procedure TMainForm.actProjSaveExecute(Sender: TObject);
begin
  if fProj.isNotAssigned then
    exit;
  if (fProj.getFormat = pfDUB) and TDubProject(fProj.getProject).isSDL then
  begin
    fMsgs.message(DubSdlWarning, fProj, amcProj, amkWarn);
    exit;
  end;
  if checkProjectLock then
    exit;
  if fProj.filename.isNotEmpty then
    saveProj
  else
    actProjSaveAs.Execute;
end;

procedure TMainForm.actProjOpenExecute(Sender: TObject);
begin
  if checkProjectLock then
      exit;
  if fProj.isAssigned and fProj.modified and
    (dlgFileChangeClose(fProj.filename, UnsavedProj) = mrCancel) then
      exit;
  with TOpenDialog.Create(nil) do
  try
    Filter := 'DUB json|*.json|DUB sdl|*.sdl|Dexed project|*.dprj';
    if execute then
      openProj(filename.normalizePath);
  finally
    Free;
  end;
end;

procedure TMainForm.actProjEditorExecute(Sender: TObject);
var
  win: TControl = nil;
begin
  if fProj.isAssigned then
    case fProj.getFormat of
      pfDUB: win := DockMaster.GetAnchorSite(fDubProjWidg);
      pfDEXED: win := DockMaster.GetAnchorSite(fPrjCfWidg);
    end
  else win := DockMaster.GetAnchorSite(fPrjCfWidg);
  if win.isAssigned then
  begin
    win.Show;
    win.BringToFront;
  end;
end;

procedure TMainForm.actProjSourceExecute(Sender: TObject);
begin
  if fProj.isNotAssigned or not fProj.filename.fileExists then
    exit;

  if (fProj.getFormat = pfDUB) and TDubProject(fProj.getProject).isSDL then
  begin
    fMsgs.message(DubSdlWarning, fProj, amcProj, amkWarn);
    exit;
  end;

  openFile(fProj.filename);
  fDoc.isProjectDescription := true;
  if fProj.getFormat = pfDEXED then
    fDoc.Highlighter := LfmSyn
  else
    fDoc.Highlighter := JsSyn;
end;

procedure TMainForm.actProjOptViewExecute(Sender: TObject);
begin
  if fProj.isAssigned then
    dlgOkInfo(fProj.getCommandLine, 'Compilation command line');
end;

procedure TMainForm.actProjTestExecute(Sender: TObject);
begin
  if fProj.isNotAssigned or checkProjectLock then
      exit;
  fProj.test;
end;

procedure TMainForm.actProjStopCompExecute(Sender: TObject);
begin
  if fProj.isAssigned then
    fProj.stopCompilation();
end;

procedure TMainForm.actProjDscanExecute(Sender: TObject);
var
  lst: TStringList;
  prc: TProcess;
  pth: string;
  msg: string;
  i: integer;
  s1: string;
  s2: string;
begin
  if fProj.isNotAssigned then
    exit;

  pth := exeFullName('dscanner' + exeExt);
  if not pth.fileExists then
    exit;
  prc := TProcess.Create(nil);
  lst := TStringList.Create;
  try
    prc.Executable:=pth;
    prc.Options := [poUsePipes, poStderrToOutPut {$IFDEF WINDOWS}, poNewConsole{$ENDIF}];
    prc.ShowWindow:= swoHIDE;
    prc.Parameters.Add('-S');
    s1 := fProj.basePath + 'dscanner.ini';
    s2 := fProj.basePath + '.dscanner.ini';
    if s1.fileExists then
      prc.Parameters.Add('--config='+s1)
    else if s2.fileExists then
      prc.Parameters.Add('--config='+s2)
    else if not fDscanUnittests then
      prc.Parameters.Add('--skipTests');
    for i := 0 to fProj.sourcesCount-1 do
      prc.Parameters.Add(fProj.sourceAbsolute(i));
    prc.Execute;
    processOutputToStrings(prc, lst);
    while prc.Running do;
    for msg in lst do
      fMsgs.message(msg, fProj, amcProj, amkWarn);
  finally
    prc.Free;
    lst.Free;
  end;
end;

procedure TMainForm.actProjGitPullExecute(Sender: TObject);
var
  p: TProcess;
  r: TStringList;
  i: integer;
begin
  p := TProcess.Create(nil);
  r := TStringList.Create;
  try
    p.Executable := exeFullName('git' + exeExt);
    if p.Executable.fileExists then
    begin
      p.Options := [poUsePipes, poNoConsole, poStderrToOutPut];
      p.ShowWindow:= swoHIDE;
      p.Parameters.Add('pull');
      p.CurrentDirectory:= fProj.basePath;
      p.Execute;
      processOutputToStrings(p,r);
      for i := 0 to r.Count-1 do
        fMsgs.message(r[i], fProj, amcProj, amkAuto);
      while p.Running do ;
      p.Parameters.Clear;
      p.Parameters.AddStrings(['submodule', 'update', '--init', '--recursive']);
      p.Execute;
      processOutputToStrings(p,r);
      while p.Running do ;
      for i := 0 to r.Count-1 do
        fMsgs.message(r[i], fProj, amcProj, amkAuto);
    end;
  finally;
    p.Free;
    r.Free;
  end;
end;

procedure TMainForm.gitBranchMenuItemClick(sender: TObject);
var
  p: TProcess;
  r: TStringList;
  i: integer;
  b: string;
begin
  if fProj.isNotAssigned then
    exit;
  p := TProcess.Create(nil);
  r := TStringList.Create;
  b := TMenuItem(sender).Hint;
  try
    p.Executable := exeFullName('git' + exeExt);
    if p.Executable.fileExists then
    begin
      p.Options := [poUsePipes, poNoConsole, poStderrToOutPut];
      p.ShowWindow:= swoHIDE;
      p.Parameters.AddStrings(['checkout', b]);
      p.CurrentDirectory:= fProj.basePath;
      p.Execute;
      processOutputToStrings(p,r);
      while p.Running do ;
      for i := 0 to r.Count-1 do
        fMsgs.message(r[i], fProj, amcProj, amkAuto);
    end;
  finally;
    actProjGitBranchesUpd.Execute;
    p.Free;
    r.Free;
  end;
end;

procedure TMainForm.actProjGitBranchesUpdExecute(Sender: TObject);
var
  p: TProcess;
  r: TStringList;
  c: TStringList;
  i: integer;
  m: TMenuItem;
  a: boolean;
begin
  if fProj.isNotAssigned then
    exit;
  a := mnuGitBranch.Count >= 2;
  if a then
    while mnuGitBranch.Count <> 2 do
      mnuGitBranch.delete(mnuGitBranch.Count-1);
  if not DirectoryExistsUTF8(fProj.basePath + DirectorySeparator + '.git') then
    exit;
  p := TProcess.Create(nil);
  r := TStringList.Create;
  c := TStringList.create;
  try
    p.Executable := exeFullName('git' + exeExt);
    if p.Executable.fileExists then
    begin
      p.Options := [poUsePipes, poNoConsole];
      p.ShowWindow:= swoHIDE;
      p.Parameters.AddStrings(['branch', '--list']);
      p.CurrentDirectory:= fProj.basePath;
      p.Execute;
      processOutputToStrings(p,r);
      while p.Running do ;
      if not a then
      begin
        m := TMenuItem.Create(mnuGitBranch);
        m.action := actProjGitBranchesUpd;
        mnuGitBranch.Add(m);
        mnuGitBranch.AddSeparator;
      end;
      c.LoadFromStream(p.Stderr);
      for i := 0 to c.Count-1 do
        fMsgs.message(c[i], fProj, amcProj, amkAuto);
      p.Parameters.Clear();
      p.Parameters.AddStrings(['log', '--format=%B', '-n', '1', '<branch...>']);
      for i:= 0 to r.Count-1 do
      begin
        m := TMenuItem.Create(mnuGitBranch);
        m.GroupIndex := 45;
        m.RadioItem:= true;
        m.ImageIndex:=fGitIconIndex;
        m.OnClick:= @gitBranchMenuItemClick;
        if r[i][1] = '*' then
        begin
          m.Hint:= Trim(r[i][2..r[i].length]);
          m.Checked:= true;
        end
        else m.Hint:= Trim(r[i][1..r[i].length]);
        c.Clear;
        p.Parameters.Delete(4);
        p.Parameters.AddStrings(m.Hint);
        p.Execute();
        processOutputToStrings(p, c);
        while p.Running do ;
        if not c.Count.equals(0) then
          m.caption := m.Hint + '  [last commit: ' + c[0] + ' ]'
        else
          // e.g "(HEAD detached on ...)"
          m.caption := m.Hint;
        mnuGitBranch.Add(m);
      end;
    end;
  finally
    p.Free;
    r.Free;
    c.Free;
  end;
end;

procedure TMainForm.actProjOpenGroupExecute(Sender: TObject);
begin
  if fProjectGroup.groupModified then
  begin
    if dlgFileChangeClose(fProjectGroup.groupFilename, UnsavedPGrp) = mrCancel then
      exit;
  end;
  with TOpenDialog.Create(nil) do
  try
    Filter := 'Dexed project groups|*.dgrp';
    if execute then
    begin
      filename := filename.normalizePath;
      fProjectGroup.closeGroup;
      fProjectGroup.openGroup(filename);
      fPrjGrpMru.Insert(0, filename);
    end;
  finally
    free;
  end;
  if fProj.isNotAssigned and (fProjectGroup.getProjectIndex < fProjectGroup.projectCount) then
    fProjectGroup.getProject(fProjectGroup.getProjectIndex).activate();
end;

procedure TMainForm.actProjSaveGroupAsExecute(Sender: TObject);
begin
  with TSaveDialog.Create(nil) do
  try
    Filter := 'Dexed project groups|*.dgrp';
    if fProjectGroup.groupFilename.fileExists then
      InitialDir := fProjectGroup.groupFilename.extractFileDir;
    if execute then
      fProjectGroup.saveGroup(filename.normalizePath);
  finally
    free;
  end;
end;

procedure TMainForm.actProjSaveGroupExecute(Sender: TObject);
begin
  if not fProjectGroup.groupFilename.fileExists then
    actProjSaveGroupAs.Execute
  else
    fProjectGroup.saveGroup(fProjectGroup.groupFilename);
end;

procedure TMainForm.actProjSelUngroupedExecute(Sender: TObject);
begin
  if fFreeProj.isAssigned then
    fFreeProj.activate;
end;

procedure TMainForm.actProjSetEnvExecute(Sender: TObject);
var
  p: TDubProject;
  e: TStrings;
  s: string;
begin
  if fProj.isNotAssigned or (fProj.getFormat <> pfDUB) then
    exit;
  p := TDubProject(fProj.getProject);
  e := p.getPersistentEnvironment;
  s := e.strictText;
  if InputQuery('Persistent project environment', 'values (key=value;key=value;...)', s) then
  begin
    e.Clear;
    e.AddText(s);
  end;
end;

procedure TMainForm.actNewGroupExecute(Sender: TObject);
begin
  if fProjectGroup.groupModified then
  begin
    if dlgFileChangeClose(fProjectGroup.groupFilename, UnsavedPGrp) = mrCancel then
      exit;
  end;
  fProjectGroup.closeGroup;
end;

procedure TMainForm.actProjAddToGroupExecute(Sender: TObject);
begin
  if fFreeProj.isNotAssigned or fFreeProj.inGroup or
    not fFreeProj.filename.fileExists then
      exit;
  fProjectGroup.addProject(fFreeProj);
  fFreeProj := nil;
end;

procedure TMainForm.compileGroup(async: TAsyncWait);
var
  i, j: integer;
begin
  if checkProjectLock then
    exit;
  if fProjectGroup.projectCount.equals(0) then
    exit;
  fProjBeforeGroup := fProj;
  fGroupCompilationCnt := 0;
  fIsCompilingGroup := true;
  fMsgs.message('start compiling a project group...', nil, amcAll, amkInf);
  if fAppliOpts.showBuildDuration then
    fCompStart := GetTickCount64;
  for i:= 0 to fProjectGroup.projectCount-1 do
  begin
    fProjectGroup.getProject(i).activate;
    // customized async mode: wait
    if not fProjectGroup.projectIsAsync(i) and (async = awCustom) then
    begin
      while fGroupCompilationCnt <> i do
        Application.ProcessMessages;
      for j:= 0 to i-1 do
        if not fProjectGroup.getProject(j).compiled then
      begin
        fMsgs.message('group compilation has stopped because of a failure',
          nil, amcAll, amkErr);
        fIsCompilingGroup := false;
        break;
      end;
    end;
    fProj.compile;
    // sequential
    if (async = awNo) then
    begin
      while fProjActionsLock do
        Application.ProcessMessages;
      if not fProj.compiled then
      begin
        fMsgs.message('group compilation has stopped because of a failure',
          nil, amcAll, amkErr);
        fIsCompilingGroup := false;
        break;
      end;
    end
  end;
end;

procedure TMainForm.actProjGroupCompileExecute(Sender: TObject);
begin
  compileGroup(awYes);
end;

procedure TMainForm.actProjGroupCompileSyncExecute(Sender: TObject);
begin
  compileGroup(awNo);
end;

procedure TMainForm.actProjGroupCompileCustomSyncExecute(Sender: TObject);
begin
  compileGroup(awCustom);
end;

procedure TMainForm.actProjNewGroupExecute(Sender: TObject);
begin
  if fProjectGroup.groupModified and
    (dlgFileChangeClose(fProjectGroup.groupFilename, UnsavedPGrp) = mrCancel) then
      exit;
  fProjectGroup.closeGroup;
  if fFreeProj.isAssigned then
    fFreeProj.activate;
end;
{$ENDREGION}

end.
