{
    MainWindow используется для создания главного окна Малик Эмулятора.
    Этот исходный текст является частью Малик Эмулятора.

    Следующие файлы используются этим исходным текстом:
        mainwindow.lfm
    На них так же распространяются те же права, как и на этот исходный текст.

    Copyright © 2016–2017, 2019–2022 Малик Разработчик

    Малик Эмулятор – свободная программа: вы можете перераспространять её и/или
    изменять её на условиях Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Малик Эмулятор распространяется в надежде, что он может быть полезен,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЁННЫХ ЦЕЛЕЙ. Подробнее см. в Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <http://www.gnu.org/licenses/>.
}

unit MainWindow;

{$MODE DELPHI}

interface

uses
    Classes,
    SysUtils,
    Forms,
    Graphics,
    Dialogs,
    Controls,
    StdCtrls,
    ComCtrls,
    ExtCtrls,
    Menus,
    Lang,
    IOStreams,
    FileIO,
    SettingFiles,
    Manifests,
    EmulConstants,
    EmulProgrammes,
    StaticRecompilers,
    EmulatorInterfaces,
    EmulationWindow,
    ProgrammePropertiesWindow,
    InstallWindow,
    InstallProcessWindow,
    UninstallProcessWindow,
    RunningListWindow,
    SettingsWindow,
    AboutWindow,
    ProgrammesListFrame;

{%region public }
const
    MAX_RUNNING_PROGRAMMES = int(4);
    MAX_RECENT_PROGRAMMES = int(10);

type
    ProgrammeInfoWithID = class;
    TMainForm = class;

    ProgrammeInfoWithID_Array1d = packed array of ProgrammeInfoWithID;

    ProgrammeInfoWithID = class(ProgrammeInfo)
    strict private
        id: AnsiString;
    public
        constructor create(const id, directory: AnsiString);
        function getID(): AnsiString;
    end;

    TMainForm = class(TEmulatorFormWithDialog, _Interface, MainWindowInterface)
        infoAgeCatBack: TImage;
        infoAgeCat: TLabel;
        mainMenu: TMainMenu;
        programmeMenu: TMenuItem;
        programmeLaunch: TMenuItem;
        programmeDebug: TMenuItem;
        programmeProperties: TMenuItem;
        _001: TMenuItem;
        programmeInstall: TMenuItem;
        programmeUninstall: TMenuItem;
        programmeMove: TMenuItem;
        _002: TMenuItem;
        programmeRunningList: TMenuItem;
        _003: TMenuItem;
        programmeEmulatorSettings: TMenuItem;
        _004: TMenuItem;
        programmeCloseWindow: TMenuItem;
        viewMenu: TMenuItem;
        viewRecent: TMenuItem;
        viewGames: TMenuItem;
        viewApps: TMenuItem;
        helpMenu: TMenuItem;
        helpAbout: TMenuItem;
        menuBevel: TBevel;
        programmesTabs: TPageControl;
        recentTab: TTabSheet;
        recentList: TProgrammesList;
        gamesTab: TTabSheet;
        gamesList: TProgrammesList;
        appsTab: TTabSheet;
        appsList: TProgrammesList;
        infoPanel: TPanel;
        infoIcon: TImage;
        infoName: TMemo;
        infoDescription: TMemo;
        infoLaunch: TButton;
        runningList: TRunningListForm;
        procedure formShow(sender: TObject);
        procedure formClose(sender: TObject; var action: TCloseAction);
        procedure programmeLaunchClick(sender: TObject);
        procedure programmeDebugClick(sender: TObject);
        procedure programmePropertiesClick(sender: TObject);
        procedure programmeInstallClick(sender: TObject);
        procedure programmeUninstallClick(sender: TObject);
        procedure programmeMoveClick(sender: TObject);
        procedure programmeRunningListClick(sender: TObject);
        procedure programmeEmulatorSettingsClick(sender: TObject);
        procedure programmeCloseWindowClick(sender: TObject);
        procedure viewPageClick(sender: TObject);
        procedure helpAboutClick(sender: TObject);
        procedure programmesTabsChange(sender: TObject);
        procedure programmesListSelectionChange(sender: TObject; user: boolean);
    private
        emulatorDirectory: AnsiString;
        settings: AbstractInitializationFile;
        programmes: ProgrammeInfoWithID_Array1d;
        {
            Некоторые ячейки массива programmes могут быть равны нулевой ссылке из-за несуществования некоторых папок или файлов манифеста.
            Это может случиться из-за того, что в списке установленных программ могут оказаться несуществующие папки.
        }
        runningProgrammes: EmulationWindowInterface_Array1d;
        runningProgrammesCount: int;
        procedure clearInfoBox();
        procedure fillProgrammesLists();
        procedure fillProgrammesRecent();
        procedure launchSelectedProgramme(debug: boolean);
        procedure registerProgramme(const programmeDirectory: AnsiString);
        procedure unregisterProgramme(const id: AnsiString);
        procedure makeRecentProgramme(const id: AnsiString);
        function getProgrammeInfo(const id: AnsiString): ProgrammeInfoWithID;
        function getSelectedProgrammeInfo(): ProgrammeInfo;
    public
        constructor create(theOwner: TComponent); override;
        destructor destroy; override;
        procedure afterConstruction(); override;
        procedure beforeDestruction(); override;
        { _Interface }
        function getClass(): _Class;
        function asObject(): TObject;
        { MainWindowInterface }
        procedure showMainWindow();
        procedure notifyTerminated(window: EmulationWindowInterface);
        procedure saveSettings();
        procedure removeSetting(const section, key: AnsiString);
        procedure setSetting(const section, key, value: AnsiString);
        function isSectionExists(const section: AnsiString): boolean;
        function isKeyExists(const section, key: AnsiString): boolean;
        function getSetting(const section, key, defaultValue: AnsiString): AnsiString;
        function getSections(): AnsiString_Array1d;
        function getKeys(const section: AnsiString): AnsiString_Array1d;
        function getEmulatorsCount(): int;
        function getEmulator(index: int): EmulationWindowInterface;

    strict private
        INSTANCE: TMainForm; static;
    public
        class procedure createInstance();
    end;
{%endregion}

implementation

{$R *.LFM}

{%region routine }
    function ProgrammeInfoWithID_Array1d_create(length: int): ProgrammeInfoWithID_Array1d;
    begin
        setLength(result, length);
    end;
{%endregion}

{%region ProgrammeInfoWithID }
    constructor ProgrammeInfoWithID.create(const id, directory: AnsiString);
    begin
        inherited create(directory);
        self.id := id;
    end;

    function ProgrammeInfoWithID.getID(): AnsiString;
    begin
        result := id;
    end;
{%endregion}

{%region TMainForm }
    class procedure TMainForm.createInstance();
    begin
        if INSTANCE = nil then begin
            application.createForm(self, INSTANCE);
        end;
    end;

    constructor TMainForm.create(theOwner: TComponent);
    var
        i: int;
        j: int;
        s: AnsiString;
        r: StaticRecompiler;
        a: AdjustableRecompiler;
    begin
        inherited create(theOwner);
        settings := NormalInitializationFile.create(emulatorDirectory + 'emulator.ini');
        emulatorDirectory := getEmulatorDirectory();
        runningList := TRunningListForm.create(self);
        runningProgrammes := EmulationWindowInterface_Array1d_create(MAX_RUNNING_PROGRAMMES);
        for i := getRecompilersCount() - 1 downto 0 do begin
            r := getRecompiler(i);
            r.getInterface(stringToGUID(ADJUSTABLE_RECOMPILER_GUID), a);
            if a = nil then begin
                continue;
            end;
            s := 'platform: ' + a.getPlatformName();
            if not settings.isSectionExists(s) then begin
                a.setDefaults();
                continue;
            end;
            a.setAlignment(getSetting(s, KEY_ALIGNMENT, '1') = '1');
            a.setCompressionLevel(parseDecInt(getSetting(s, KEY_COMPRESSION, ''), 0));
            a.setStackSize(parseDecInt(getSetting(s, KEY_STACK, ''), 1));
            j := parseDecInt(getSetting(s, KEY_HEAP, ''), 4);
            a.setHeapSize(j shl 10);
            a.setDescriptorsSize(j shl 5);
            a.setOutputExecutableFileName(getSetting(s, KEY_EXECUTABLE, '/executable'));
        end;
    end;

    destructor TMainForm.destroy;
    begin
        runningList.free();
        saveSettings();
        settings.free();
        inherited destroy;
    end;

    procedure TMainForm.formShow(sender: TObject);
    begin
        TProgrammesList(programmesTabs.activePage.controls[0]).programmesList.setFocus();
    end;

    procedure TMainForm.formClose(sender: TObject; var action: TCloseAction);
    begin
        if (runningProgrammesCount = 0) and (runningList.visible = false) then begin
            action := caFree;
            application.terminate();
        end else begin
            action := caHide;
        end;
    end;

    procedure TMainForm.programmeLaunchClick(sender: TObject);
    begin
        launchSelectedProgramme(false);
    end;

    procedure TMainForm.programmeDebugClick(sender: TObject);
    begin
        launchSelectedProgramme(true);
    end;

    procedure TMainForm.programmePropertiesClick(sender: TObject);
    var
        propertiesDialog: TProgrammePropertiesForm;
        info: ProgrammeInfo;
        pman: ProgrammeManifest;
        sizes: AnsiString_Array1d;
        screenWidth: int;
        screenHeight: int;
    begin
        info := getSelectedProgrammeInfo();
        if info = nil then begin
            exit;
        end;
        propertiesDialog := TProgrammePropertiesForm.create(self);
        try
            pman := ProgrammeManifest.create();
            try
                info.loadManifest(pman);
                sizes := getComponents(pman.getValue(MANIFEST_PROPERTY_MALIK_SCREEN_SIZE));
                if length(sizes) >= 2 then begin
                    screenWidth := parseDecInt(sizes[0], DEFAULT_SCREEN_WIDTH);
                    screenHeight := parseDecInt(sizes[1], DEFAULT_SCREEN_HEIGHT);
                end else begin
                    screenWidth := DEFAULT_SCREEN_WIDTH;
                    screenHeight := DEFAULT_SCREEN_HEIGHT;
                end;
                propertiesDialog.setProperties(info.getProgrammeDirectory(), pman.getValue(MANIFEST_PROPERTY_PROGRAMME_DESCRIPTION), screenWidth, screenHeight);
                if openDialog(propertiesDialog) = mrOK then begin
                    pman.setValue(MANIFEST_PROPERTY_PROGRAMME_DESCRIPTION, trim(propertiesDialog.getDescription()));
                    screenWidth := propertiesDialog.getScreenWidth();
                    screenHeight := propertiesDialog.getScreenHeight();
                    if (screenWidth = DEFAULT_SCREEN_WIDTH) and (screenHeight = DEFAULT_SCREEN_HEIGHT) then begin
                        pman.setValue(MANIFEST_PROPERTY_MALIK_SCREEN_SIZE, '');
                    end else begin
                        pman.setValue(MANIFEST_PROPERTY_MALIK_SCREEN_SIZE, makeStringOf(toStringArray1d([
                            toDecString(screenWidth), toDecString(screenHeight)
                        ])));
                    end;
                    info.saveManifest(pman);
                    programmesListSelectionChange(TProgrammesList(programmesTabs.activePage.controls[0]).programmesList, false);
                end;
            finally
                pman.free();
            end;
        finally
            propertiesDialog.free();
        end;
    end;

    procedure TMainForm.programmeInstallClick(sender: TObject);
    var
        installDialog: TInstallForm;
        progressDialog: TInstallProcessForm;
        recompiler: StaticRecompiler;
        archive: AnsiString;
        destinationDirectory: AnsiString;
        programmeDirectory: AnsiString;
    begin
        installDialog := TInstallForm.create(self);
        try
            if openDialog(installDialog) <> mrOK then begin
                exit;
            end;
            recompiler := installDialog.getRecompiler();
            archive := installDialog.getArchive();
            destinationDirectory := installDialog.getDestinationDirectory();
        finally
            installDialog.free();
        end;
        progressDialog := TInstallProcessForm.create(self);
        try
            progressDialog.setInstallParameters(recompiler, archive, destinationDirectory);
            openDialog(progressDialog);
            programmeDirectory := progressDialog.getProgrammeDirectory();
        finally
            progressDialog.free();
        end;
        if programmeDirectory = '' then begin
            exit;
        end;
        registerProgramme(programmeDirectory);
        fillProgrammesLists();
    end;

    procedure TMainForm.programmeUninstallClick(sender: TObject);
    var
        i: int;
        running: EmulationWindowInterface_Array1d;
        uninstallingProgramme: ProgrammeInfoWithID;
        processDialog: TUninstallProcessForm;
        uninstalled: boolean;
        dir: AnsiString;
        id: AnsiString;
    begin
        uninstallingProgramme := ProgrammeInfoWithID(getSelectedProgrammeInfo());
        dir := uninstallingProgramme.getProgrammeDirectory();
        running := runningProgrammes;
        for i := runningProgrammesCount - 1 downto 0 do begin
            if running[i].getProgrammeDirectory() = dir then begin
                openDialog('Ошибка', 'Нельзя удалить программу, если она выполняется.', mtError, [mbClose]);
                exit;
            end;
        end;
        id := uninstallingProgramme.getID();
        processDialog := TUninstallProcessForm.create(self);
        try
            processDialog.setUninstallID(id);
            openDialog(processDialog);
            uninstalled := processDialog.isUninstalled();
        finally
            processDialog.free();
        end;
        if uninstalled then begin
            unregisterProgramme(id);
            fillProgrammesLists();
        end;
    end;

    procedure TMainForm.programmeMoveClick(sender: TObject);
    var
        info: ProgrammeInfoWithID;
        oldDirectory: AnsiString;
        newDirectory: AnsiString;
        edir: AnsiString;
        len: int;
    begin
        info := ProgrammeInfoWithID(getSelectedProgrammeInfo());
        if info = nil then begin
            exit;
        end;
        edir := emulatorDirectory;
        oldDirectory := info.getProgrammeDirectory();
        if startsWith(edir + DIRECTORY_APPS, oldDirectory) then begin
            len := length(edir) + length(DIRECTORY_APPS);
            newDirectory := edir + DIRECTORY_GAMES + copy(oldDirectory, len + 1, length(oldDirectory) - len);
        end else
        if startsWith(edir + DIRECTORY_GAMES, oldDirectory) then begin
            len := length(edir) + length(DIRECTORY_GAMES);
            newDirectory := edir + DIRECTORY_APPS + copy(oldDirectory, len + 1, length(oldDirectory) - len);
        end else begin
            exit;
        end;
        delete(oldDirectory, length(oldDirectory), 1);
        delete(newDirectory, length(newDirectory), 1);
        try
            if not move(oldDirectory, newDirectory) then begin
                raise IOException.create('Не удалось переместить папку ' + oldDirectory + ' на новое место под новым именем ' + newDirectory);
            end;
            len := length(edir);
            setSetting(SECTION_PROGRAMMES, info.getID(), copy(newDirectory, len + 1, length(newDirectory) - len));
            saveSettings();
            fillProgrammesLists();
        except
            on e: Lang.Exception do begin
                openDialog('Ошибка',
                    'При выполнении команды произошла ошибка.' + (LINE_ENDING +
                    'Класс: ') + e.getClass().getName() + (LINE_ENDING +
                    'Сообщение: ') + e.getMessage(), mtError, [mbClose]
                );
            end;
            on e: SysUtils.Exception do begin
                openDialog('Ошибка',
                    'При выполнении команды произошла ошибка.' + (LINE_ENDING +
                    'Класс: ') + (e.unitName() + '.' + e.className()) + (LINE_ENDING +
                    'Сообщение: ') + e.message, mtError, [mbClose]
                );
            end;
        end;
    end;

    procedure TMainForm.programmeRunningListClick(sender: TObject);
    begin
        runningList.showOnTop();
    end;

    procedure TMainForm.programmeEmulatorSettingsClick(sender: TObject);
    var
        dialog: TForm;
    begin
        dialog := TSettingsForm.create(self);
        try
            openDialog(dialog);
        finally
            dialog.free();
        end;
    end;

    procedure TMainForm.programmeCloseWindowClick(sender: TObject);
    begin
        close();
    end;

    procedure TMainForm.viewPageClick(sender: TObject);
    begin
        programmesTabs.pages[TMenuItem(sender).tag].show();
    end;

    procedure TMainForm.helpAboutClick(sender: TObject);
    var
        dialog: TForm;
    begin
        dialog := TAboutForm.create(self);
        try
            openDialog(dialog);
        finally
            dialog.free();
        end;
    end;

    procedure TMainForm.programmesTabsChange(sender: TObject);
    var
        list: TListBox;
    begin
        viewMenu.items[programmesTabs.activePageIndex].checked := true;
        list := TProgrammesList(programmesTabs.activePage.controls[0]).programmesList;
        list.setFocus();
        programmesListSelectionChange(list, false);
    end;

    procedure TMainForm.programmesListSelectionChange(sender: TObject; user: boolean);
    var
        len: int;
        icon: TIcon;
        info: ProgrammeInfo;
        pman: ProgrammeManifest;
        s: AnsiString;
        t: AnsiString;
    begin
        if sender <> TProgrammesList(programmesTabs.activePage.controls[0]).programmesList then begin
            exit;
        end;
        info := TProgrammesList(TControl(sender).parent).getSelectedProgrammeInfo();
        if info = nil then begin
            clearInfoBox();
            exit;
        end;
        try
            pman := ProgrammeManifest.create();
            try
                icon := infoIcon.picture.icon;
                info.loadManifest(pman);
                info.loadIcon(icon);
                findBestIconSize(icon, 128);
                s := trim(pman.getValue(MANIFEST_PROPERTY_AGE_CATEGORY));
                len := length(s);
                if ((len = 3) and (s[1] = '1') and (s[2] in ['0'..'9']) and (s[3] = '+')) or ((len = 2) and (s[1] in ['0'..'9']) and (s[2] = '+')) then begin
                    infoAgeCatBack.visible := true;
                    infoAgeCat.visible := true;
                    infoAgeCat.caption := s;
                end else begin
                    infoAgeCatBack.visible := false;
                    infoAgeCat.visible := false;
                    infoAgeCat.caption := '';
                end;
                infoName.text := trim(pman.getValue(MANIFEST_PROPERTY_PROGRAMME_NAME));
                s := trim(pman.getValue(MANIFEST_PROPERTY_PROGRAMME_DESCRIPTION));
                if s = '' then begin
                    s := 'Описание отсутствует. Если хотите самостоятельно добавить описание к программе, нажмите пункт меню Программа → Свойства.';
                end;
                s := s + (LINE_ENDING + LINE_ENDING +
                    'Версия: ') + trim(pman.getValue(MANIFEST_PROPERTY_PROGRAMME_VERSION)) + (LINE_ENDING + LINE_ENDING +
                    'Разработчик: ') + trim(pman.getValue(MANIFEST_PROPERTY_PROGRAMME_VENDOR))
                ;
                t := trim(pman.getValue(MANIFEST_PROPERTY_PROGRAMME_SITE));
                if t <> '' then begin
                    s := s + (LINE_ENDING + LINE_ENDING + 'Интернет: ') + t;
                end;
                t := trim(pman.getValue(MANIFEST_PROPERTY_SOURCE_PLATFORM));
                if t <> '' then begin
                    s := s + (LINE_ENDING + LINE_ENDING + 'Исходная платформа: ') + t;
                end;
                infoDescription.text := s;
                infoLaunch.enabled := true;
                programmeLaunch.enabled := true;
                programmeDebug.enabled := true;
                programmeProperties.enabled := true;
                programmeUninstall.enabled := true;
                s := info.getProgrammeDirectory();
                if startsWith(emulatorDirectory + DIRECTORY_GAMES, s) then begin
                    programmeMove.caption := 'Переместить &в Приложения';
                    programmeMove.enabled := true;
                end else
                if startsWith(emulatorDirectory + DIRECTORY_APPS, s) then begin
                    programmeMove.caption := 'Переместить &в Игры';
                    programmeMove.enabled := true;
                end else begin
                    programmeMove.caption := 'Переместить';
                    programmeMove.enabled := false;
                end;
            finally
                pman.free();
            end;
        except
            clearInfoBox();
        end;
    end;

    procedure TMainForm.clearInfoBox();
    begin
        infoIcon.picture.graphic := nil;
        infoAgeCat.caption := '';
        infoAgeCat.visible := false;
        infoAgeCatBack.visible := false;
        infoName.text := '';
        infoDescription.text := '';
        infoLaunch.enabled := false;
        programmeLaunch.enabled := false;
        programmeDebug.enabled := false;
        programmeProperties.enabled := false;
        programmeUninstall.enabled := false;
        programmeMove.enabled := false;
        programmeMove.caption := 'Переместить';
    end;

    procedure TMainForm.fillProgrammesLists();
    var
        i: int;
        count: int;
        recentCount: int;
        gamesCount: int;
        appsCount: int;
        otherCount: int;
        all: ProgrammeInfoWithID_Array1d;
        recent: ProgrammeInfo_Array1d;
        games: ProgrammeInfo_Array1d;
        apps: ProgrammeInfo_Array1d;
        other: ProgrammeInfo_Array1d;
        info: ProgrammeInfoWithID;
        otherTab: TTabSheet;
        otherList: TProgrammesList;
        viewOther: TMenuItem;
        ids: AnsiString_Array1d;
        id: AnsiString;
        edir: AnsiString;
        directory: AnsiString;
        recentIDs: AnsiString_Array1d;
    begin
        all := programmes;
        for i := 0 to length(all) - 1 do begin
            all[i].free();
            all[i] := nil;
        end;
        ids := getKeys(SECTION_PROGRAMMES);
        count := length(ids);
        for i := 0 to count - 1 do begin
            if ids[i] = KEY_RECENT then begin
                arraycopy(ids, i + 1, ids, i, count - i - 1);
                dec(count);
                break;
            end;
        end;
        recentCount := 0;
        gamesCount := 0;
        appsCount := 0;
        otherCount := 0;
        all := ProgrammeInfoWithID_Array1d_create(count);
        recent := ProgrammeInfo_Array1d_create(count);
        games := ProgrammeInfo_Array1d_create(count);
        apps := ProgrammeInfo_Array1d_create(count);
        other := ProgrammeInfo_Array1d_create(count);
        edir := emulatorDirectory;
        programmes := all;
        { Формируем список всех программ }
        for i := 0 to count - 1 do begin
            id := ids[i];
            directory := trim(getSetting(SECTION_PROGRAMMES, id, ''));
            if not endsWith(DIRECTORY_SEPARATOR, directory) then begin
                directory := directory + DIRECTORY_SEPARATOR;
            end;
            if (length(directory) < 2) or (not (directory[1] in ['A'..'Z', 'a'..'z'])) or (directory[2] <> ':') then begin
                directory := edir + directory;
            end;
            if not fileExists(directory + ProgrammeInfo.MANIFEST_FILE_NAME) then begin
                continue;
            end;
            info := ProgrammeInfoWithID.create(id, directory);
            all[i] := info;
            if startsWith(edir + DIRECTORY_GAMES, directory) then begin
                games[gamesCount] := info;
                inc(gamesCount);
            end else
            if startsWith(edir + DIRECTORY_APPS, directory) then begin
                apps[appsCount] := info;
                inc(appsCount);
            end else begin
                other[otherCount] := info;
                inc(otherCount);
            end;
        end;
        recentIDs := getComponents(getSetting(SECTION_PROGRAMMES, KEY_RECENT, ''));
        for i := 0 to length(recentIDs) - 1 do begin
            info := getProgrammeInfo(recentIDs[i]);
            if info = nil then begin
                continue;
            end;
            recent[recentCount] := info;
            inc(recentCount);
        end;
        { Заполняем списки }
        recentList.setProgrammesInfo(recent, recentCount);
        gamesList.setProgrammesInfo(games, gamesCount);
        appsList.setProgrammesInfo(apps, appsCount);
        if (programmesTabs.pageCount = 4) and (otherCount = 0) then begin
            recentTab.show();
            otherTab := programmesTabs.pages[3];
            otherList := otherTab.controls[0] as TProgrammesList;
            otherList.free();
            otherTab.free();
            viewMenu.items[3].free();
            exit;
        end;
        if otherCount = 0 then begin
            exit;
        end;
        if programmesTabs.pageCount = 3 then begin
            otherTab := programmesTabs.addTabSheet();
            otherList := TProgrammesList.create(otherTab);
            viewOther := TMenuItem.create(self);
            otherTab.caption := 'Прочие';
            otherTab.insertControl(otherList);
            otherList.align := alClient;
            otherList.programmesList.onSelectionChange := programmesListSelectionChange;
            viewMenu.insert(3, viewOther);
            viewOther.autoCheck := true;
            viewOther.caption := 'П&рочие';
            viewOther.groupIndex := 1;
            viewOther.radioItem := true;
            viewOther.shortCut := 16436;
            viewOther.tag := 3;
            viewOther.onClick := viewPageClick;
        end else begin
            otherTab := programmesTabs.pages[3];
            otherList := otherTab.controls[0] as TProgrammesList;
        end;
        otherList.setProgrammesInfo(other, otherCount);
    end;

    procedure TMainForm.fillProgrammesRecent();
    var
        i: int;
        len: int;
        recentCount: int;
        info: ProgrammeInfoWithID;
        recent: ProgrammeInfo_Array1d;
        recentIDs: AnsiString_Array1d;
    begin
        recentIDs := getComponents(getSetting(SECTION_PROGRAMMES, KEY_RECENT, ''));
        len := length(recentIDs);
        recent := ProgrammeInfo_Array1d_create(len);
        recentCount := 0;
        for i := 0 to len - 1 do begin
            info := getProgrammeInfo(recentIDs[i]);
            if info = nil then begin
                continue;
            end;
            recent[recentCount] := info;
            inc(recentCount);
        end;
        recentList.setProgrammesInfo(recent, recentCount);
    end;

    procedure TMainForm.launchSelectedProgramme(debug: boolean);
    var
        id: AnsiString;
        dir: AnsiString;
        info: ProgrammeInfoWithID;
        emulator: TEmulationForm;
        f: EmulationWindowInterface;
        e: EmulationWindowInterface_Array1d;
        c: int;
        i: int;
    begin
        info := ProgrammeInfoWithID(getSelectedProgrammeInfo());
        if info = nil then begin
            exit;
        end;
        e := runningProgrammes;
        c := runningProgrammesCount;
        dir := info.getProgrammeDirectory();
        for i := c - 1 downto 0 do begin
            f := e[i];
            if f.getProgrammeDirectory() = dir then begin
                f.showEmulationWindow();
                if debug then begin
                    f.showDisassemblerWindow();
                end;
                exit;
            end;
        end;
        if c = length(e) then begin
            openDialog('Слишком много запущенных программ',
                'Чтобы не перегружать процессор и память компьютера, максимальное количество одновременно выполняющихся программ ограничено ' + toDecString(c) +
                '. Закройте окно любой программы и повторите попытку.', mtError, [mbClose]
            );
            exit;
        end;
        emulator := TEmulationForm.create(self);
        try
            id := info.getID();
            e[c] := emulator;
            runningProgrammesCount := c + 1;
            emulator.setProgramme(id, info);
            emulator.setDebug(debug);
            emulator.show();
            emulator.runProgramme();
            makeRecentProgramme(id);
            fillProgrammesRecent();
            saveSettings();
            runningList.updateList();
        except
            on e: Lang.Exception do begin
                emulator.enabled := false;
                openDialog('Ошибка',
                    'При выполнении команды произошла ошибка.' + (LINE_ENDING +
                    'Класс: ') + e.getClass().getName() + (LINE_ENDING +
                    'Сообщение: ') + e.getMessage(), mtError, [mbClose]
                );
                emulator.free();
            end;
            on e: SysUtils.Exception do begin
                emulator.enabled := false;
                openDialog('Ошибка',
                    'При выполнении команды произошла ошибка.' + (LINE_ENDING +
                    'Класс: ') + (e.unitName() + '.' + e.className()) + (LINE_ENDING +
                    'Сообщение: ') + e.message, mtError, [mbClose]
                );
                emulator.free();
            end;
            else begin
                emulator.free();
            end;
        end;
    end;

    procedure TMainForm.registerProgramme(const programmeDirectory: AnsiString);
    var
        len: int;
        edir: AnsiString;
        wdir: AnsiString;
        id: AnsiString;
    begin
        edir := emulatorDirectory;
        len := length(edir);
        if startsWith(edir, programmeDirectory) then begin
            wdir := copy(programmeDirectory, len + 1, length(programmeDirectory) - len);
        end else begin
            wdir := programmeDirectory;
        end;
        repeat
            id := toHexString(random($100000000), 8);
        until not isKeyExists(SECTION_PROGRAMMES, id);
        setSetting(SECTION_PROGRAMMES, id, wdir);
        makeRecentProgramme(id);
        saveSettings();
        fillProgrammesLists();
    end;

    procedure TMainForm.unregisterProgramme(const id: AnsiString);
    var
        i: int;
        len: int;
        ids: AnsiString_Array1d;
        newIds: AnsiString_Array1d;
    begin
        removeSetting(SECTION_PROGRAMMES, id);
        removeSetting(SECTION_WINDOWS, id);
        ids := getComponents(getSetting(SECTION_PROGRAMMES, KEY_RECENT, ''));
        len := length(ids);
        for i := len - 1 downto 0 do begin
            if ids[i] <> id then begin
                continue;
            end;
            newIds := String_Array1d_create(len - 1);
            arraycopy(ids, 0, newIds, 0, i);
            arraycopy(ids, i + 1, newIds, i, len - i - 1);
            setSetting(SECTION_PROGRAMMES, KEY_RECENT, makeStringOf(newIds));
        end;
        saveSettings();
        fillProgrammesLists();
    end;

    procedure TMainForm.makeRecentProgramme(const id: AnsiString);
    var
        i: int;
        j: int;
        len: int;
        ids: AnsiString_Array1d;
        newIds: AnsiString_Array1d;
    begin
        ids := getComponents(getSetting(SECTION_PROGRAMMES, KEY_RECENT, ''));
        len := length(ids);
        for i := 0 to len - 1 do begin
            if ids[i] <> id then begin
                continue;
            end;
            if i = 0 then begin
                exit;
            end;
            for j := i downto 1 do begin
                ids[j] := ids[j - 1];
            end;
            ids[0] := id;
            setSetting(SECTION_PROGRAMMES, KEY_RECENT, makeStringOf(ids));
            exit;
        end;
        if len < MAX_RECENT_PROGRAMMES then begin
            newIds := ids;
            ids := String_Array1d_create(len + 1);
            arraycopy(newIds, 0, ids, 0, len);
            inc(len);
        end;
        for i := len - 1 downto 1 do begin
            ids[i] := ids[i - 1];
        end;
        ids[0] := id;
        setSetting(SECTION_PROGRAMMES, KEY_RECENT, makeStringOf(ids));
    end;

    function TMainForm.getProgrammeInfo(const id: AnsiString): ProgrammeInfoWithID;
    var
        i: int;
        p: ProgrammeInfoWithID_Array1d;
        info: ProgrammeInfoWithID;
    begin
        result := nil;
        p := programmes;
        for i := 0 to length(p) - 1 do begin
            info := p[i];
            if (info = nil) or (info.getID() <> id) then begin
                continue;
            end;
            result := info;
            exit;
        end;
    end;

    function TMainForm.getSelectedProgrammeInfo(): ProgrammeInfo;
    begin
        result := TProgrammesList(programmesTabs.activePage.controls[0]).getSelectedProgrammeInfo();
    end;

    procedure TMainForm.afterConstruction();
    var
        x: int;
        y: int;
        c: AnsiString_Array1d;
    begin
        inherited afterConstruction();
        x := (screen.width - self.width) div 2;
        y := (screen.height - self.height) div 2;
        c := getComponents(getSetting(SECTION_WINDOWS, KEY_MAIN, toDecString(x) + ',' + toDecString(y)));
        if length(c) >= 2 then begin
            left := parseDecInt(c[0], x);
            top := parseDecInt(c[1], y);
        end else begin
            left := x;
            top := y;
        end;
        fillProgrammesLists();
    end;

    procedure TMainForm.beforeDestruction();
    var
        i: int;
        p: ProgrammeInfoWithID_Array1d;
    begin
        setSetting(SECTION_WINDOWS, KEY_MAIN, toDecString(left) + ',' + toDecString(top));
        p := programmes;
        for i := 0 to length(p) - 1 do begin
            p[i].free();
        end;
        inherited beforeDestruction();
    end;

    function TMainForm.getClass(): _Class;
    begin
        result := ClassData.create(classType());
    end;

    function TMainForm.asObject(): TObject;
    begin
        result := self;
    end;

    procedure TMainForm.showMainWindow();
    var
        dialog: TForm;
    begin
        dialog := getOpenedDialog();
        if dialog <> nil then begin
            dialog.showOnTop();
        end else begin
            self.showOnTop();
        end;
    end;

    procedure TMainForm.notifyTerminated(window: EmulationWindowInterface);
    var
        e: EmulationWindowInterface_Array1d;
        c: int;
        i: int;
    begin
        e := runningProgrammes;
        c := runningProgrammesCount - 1;
        for i := c downto 0 do begin
            if e[i] <> window then begin
                continue;
            end;
            if c <> i then begin
                arraycopy(e, i + 1, e, i, c - i);
            end;
            e[c] := nil;
            runningProgrammesCount := c;
            break;
        end;
        runningList.updateList();
        if (runningProgrammesCount = 0) and (self.visible = false) and (runningList.visible = false) then begin
            application.terminate();
        end;
    end;

    procedure TMainForm.saveSettings();
    begin
        settings.updateFile();
    end;

    procedure TMainForm.removeSetting(const section, key: AnsiString);
    begin
        settings.deleteIdent(section, key);
    end;

    procedure TMainForm.setSetting(const section, key, value: AnsiString);
    begin
        settings.writeString(section, key, value);
    end;

    function TMainForm.isSectionExists(const section: AnsiString): boolean;
    begin
        result := settings.isSectionExists(section);
    end;

    function TMainForm.isKeyExists(const section, key: AnsiString): boolean;
    begin
        result := settings.isIdentExists(section, key);
    end;

    function TMainForm.getSetting(const section, key, defaultValue: AnsiString): AnsiString;
    begin
        result := settings.readString(section, key, defaultValue);
    end;

    function TMainForm.getSections(): AnsiString_Array1d;
    begin
        result := settings.readSections();
    end;

    function TMainForm.getKeys(const section: AnsiString): AnsiString_Array1d;
    begin
        result := settings.readSection(section);
    end;

    function TMainForm.getEmulatorsCount(): int;
    begin
        result := runningProgrammesCount;
    end;

    function TMainForm.getEmulator(index: int): EmulationWindowInterface;
    begin
        if (index < 0) or (index >= runningProgrammesCount) then begin
            raise ArrayIndexOutOfBoundsException.create(index);
        end;
        result := runningProgrammes[index];
    end;
{%endregion}

end.

