import fs from "fs"; import { ipcRenderer, shell, clipboard } from "electron"; import { dialog, getCurrentWindow } from "@electron/remote"; import { confirm } from "eez-studio-ui/dialog-electron"; import path from "path"; import React from "react"; import { observable, computed, action, runInAction, toJS, makeObservable, reaction } from "mobx"; import { observer } from "mobx-react"; import classNames from "classnames"; import * as FlexLayout from "flexlayout-react"; import { app, createEmptyFile } from "eez-studio-shared/util-electron"; import { stringCompare } from "eez-studio-shared/string"; import { initInstrumentDatabase, InstrumentDatabase, instrumentDatabases } from "eez-studio-shared/db"; import { LOCALES, getLocale, setLocale, DATE_FORMATS, getDateFormat, setDateFormat, TIME_FORMATS, getTimeFormat, setTimeFormat } from "eez-studio-shared/i10n"; import { formatBytes } from "eez-studio-shared/formatBytes"; import { showDialog, Dialog } from "eez-studio-ui/dialog"; import { Loader } from "eez-studio-ui/loader"; import { AbsoluteFileInputProperty, BooleanProperty, PropertyList, SelectProperty, StaticProperty } from "eez-studio-ui/properties"; import * as notification from "eez-studio-ui/notification"; import { Body, Header, ToolbarHeader, VerticalHeaderWithBody } from "eez-studio-ui/header-with-body"; import dbVacuum from "db-services/vacuum"; import { getMoment } from "eez-studio-shared/util"; import type { IMruItem } from "main/settings"; import { IconAction } from "eez-studio-ui/action"; import { HOME_TAB_OPEN_ICON } from "project-editor/ui-components/icons"; import { FlexLayoutContainer } from "eez-studio-ui/FlexLayout"; import { homeLayoutModels } from "./home-layout-models"; //////////////////////////////////////////////////////////////////////////////// export const COMPACT_DATABASE_MESSAGE = "It is recommended to compact the database every 30 days."; //////////////////////////////////////////////////////////////////////////////// const getIsDarkTheme = function () { return ipcRenderer.sendSync("getIsDarkTheme"); }; const setIsDarkTheme = function (value: boolean) { ipcRenderer.send("setIsDarkTheme", value); }; //////////////////////////////////////////////////////////////////////////////// const getMRU: () => IMruItem[] = function () { return ipcRenderer.sendSync("getMRU"); }; const setMRU = function (value: IMruItem[]) { ipcRenderer.send("setMRU", toJS(value)); }; ipcRenderer.on("mru-changed", async (sender: any, mru: IMruItem[]) => { function isMruChanged(mru1: IMruItem[], mru2: IMruItem[]) { if (!!mru1 != !!mru) { return true; } if (mru1.length != mru2.length) { return true; } for (let i = 0; i < mru1.length; i++) { if ( mru1[i].filePath != mru2[i].filePath || mru1[i].projectType != mru2[i].projectType ) { return true; } } return false; } if (isMruChanged(mru, settingsController.mru)) { runInAction(() => (settingsController.mru = mru)); } }); //////////////////////////////////////////////////////////////////////////////// const getShowComponentsPaletteInProjectEditor = function () { return ipcRenderer.sendSync("getShowComponentsPaletteInProjectEditor"); }; const setShowComponentsPaletteInProjectEditor = function (value: boolean) { ipcRenderer.send("setShowComponentsPaletteInProjectEditor", value); }; //////////////////////////////////////////////////////////////////////////////// class SettingsController { activetLocale = getLocale(); activeDateFormat = getDateFormat(); activeTimeFormat = getTimeFormat(); selectedDatabase: InstrumentDatabase | undefined; locale: string = getLocale(); dateFormat: string = getDateFormat(); timeFormat: string = getTimeFormat(); isDarkTheme: boolean = getIsDarkTheme(); mru: IMruItem[] = getMRU(); pythonUseCustomPath: boolean = false; pythonCustomPath: string = ""; _showComponentsPaletteInProjectEditor: boolean = getShowComponentsPaletteInProjectEditor(); constructor() { this.pythonUseCustomPath = window.localStorage.getItem("pythonUseCustomPath") == "1" ? true : false; this.pythonCustomPath = window.localStorage.getItem("pythonCustomPath") ?? ""; this.selectedDatabase = instrumentDatabases.activeDatabase; makeObservable(this, { selectedDatabase: observable, locale: observable, dateFormat: observable, timeFormat: observable, isDarkTheme: observable, mru: observable, restartRequired: computed, onLocaleChange: action.bound, onDateFormatChanged: action.bound, onTimeFormatChanged: action.bound, switchTheme: action.bound, removeItemFromMRU: action, pythonUseCustomPath: observable, pythonCustomPath: observable }); this.onThemeSwitched(); reaction( () => ({ setCustomPath: this.pythonUseCustomPath, customPythonPath: this.pythonCustomPath }), ({ setCustomPath, customPythonPath }) => { window.localStorage.setItem( "pythonUseCustomPath", setCustomPath ? "1" : "0" ); window.localStorage.setItem( "pythonCustomPath", customPythonPath ); } ); } get restartRequired() { return ( instrumentDatabases.activeDatabase?.filePath !== instrumentDatabases.activeDatabasePath || this.locale !== this.activetLocale || this.dateFormat !== this.activeDateFormat || this.timeFormat !== this.activeTimeFormat ); } onLocaleChange(value: string) { this.locale = value; setLocale(value); } onDateFormatChanged(value: string) { this.dateFormat = value; setDateFormat(value); } onTimeFormatChanged(value: string) { this.timeFormat = value; setTimeFormat(value); } onThemeSwitchedTimeout: any; switchTheme(value: boolean) { if (this.onThemeSwitchedTimeout) { return; } this.isDarkTheme = value; setIsDarkTheme(value); this.onThemeSwitched(); } onThemeSwitched() { const content = document.getElementById( "EezStudio_Content" ) as HTMLDivElement; content.style.opacity = "0"; const mainLinkElement = document.getElementById( "main-css" ) as HTMLLinkElement; const flexlayoutLinkElement = document.getElementById( "flexlayout-css" ) as HTMLLinkElement; if (this.isDarkTheme) { document.body.parentElement?.setAttribute("data-bs-theme", "dark"); mainLinkElement.href = "../eez-studio-ui/_stylesheets/main-dark.css"; flexlayoutLinkElement.href = "../../node_modules/flexlayout-react/style/dark.css"; } else { document.body.parentElement?.setAttribute("data-bs-theme", "light"); mainLinkElement.href = "../eez-studio-ui/_stylesheets/main.css"; flexlayoutLinkElement.href = "../../node_modules/flexlayout-react/style/light.css"; } this.onThemeSwitchedTimeout = setTimeout(() => { this.onThemeSwitchedTimeout = undefined; content.style.opacity = ""; }, 500); } removeItemFromMRU(mruItem: IMruItem) { const i = this.mru.indexOf(mruItem); if (i != -1) { this.mru.splice(i, 1); setMRU(this.mru); } } addDatabase(filePath: string, isActive: boolean) { instrumentDatabases.addDatabase(filePath, isActive); runInAction(() => { this.selectedDatabase = instrumentDatabases.databases.find( database => database.filePath == filePath ); }); } createNewDatabase = async () => { let defaultPath = window.localStorage.getItem("lastDatabaseSavePath"); const result = await dialog.showSaveDialog(getCurrentWindow(), { filters: [ { name: "DB files", extensions: ["db"] }, { name: "All Files", extensions: ["*"] } ], defaultPath: defaultPath ?? undefined }); const filePath = result.filePath; if (filePath) { try { createEmptyFile(filePath); await initInstrumentDatabase(filePath); const onFinish = action((isActive: boolean) => { this.addDatabase(filePath, isActive); window.localStorage.setItem( "lastDatabaseSavePath", path.dirname(filePath) ); if (isActive) { this.askForRestart(); } }); confirm( "Do you want to make this database active?", undefined, () => onFinish(true), () => onFinish(false) ); } catch (error) { notification.error(error.toString()); } } }; openDatabase = async () => { let defaultPath = window.localStorage.getItem("lastDatabaseOpenPath"); const result = await dialog.showOpenDialog(getCurrentWindow(), { properties: ["openFile"], filters: [ { name: "DB files", extensions: ["db"] }, { name: "All Files", extensions: ["*"] } ], defaultPath: defaultPath ?? undefined }); const filePaths = result.filePaths; if (filePaths && filePaths[0]) { const filePath = filePaths[0]; const onFinish = action((isActive: boolean) => { this.addDatabase(filePath, isActive); window.localStorage.setItem( "lastDatabaseOpenPath", path.dirname(filePath) ); if (isActive) { this.askForRestart(); } }); confirm( "Do you want to make this database active?", undefined, () => onFinish(true), () => onFinish(false) ); } }; askForRestart = () => { if ( instrumentDatabases.activeDatabase && instrumentDatabases.activeDatabase.filePath != instrumentDatabases.activeDatabasePath ) { confirm( "Do you want to restart the application?", "Restart is required to finish activation of new database.", this.restart ); } }; restart = () => { app.relaunch(); app.exit(); }; setAsActiveDatabase = action(() => { if (this.selectedDatabase) { instrumentDatabases.setAsActiveDatabase(this.selectedDatabase); this.askForRestart(); } }); deleteDatabase = () => { if (this.selectedDatabase) { instrumentDatabases.removeDatabase(this.selectedDatabase); } }; showDatabasePathInFolder = () => { if (this.selectedDatabase) { shell.showItemInFolder(this.selectedDatabase.filePath); } }; compactDatabase = () => { if (!this.selectedDatabase) { return; } showDialog(); }; get showComponentsPaletteInProjectEditor() { return this._showComponentsPaletteInProjectEditor; } set showComponentsPaletteInProjectEditor(value: boolean) { this._showComponentsPaletteInProjectEditor = value; setShowComponentsPaletteInProjectEditor(value); } } export const settingsController = new SettingsController(); //////////////////////////////////////////////////////////////////////////////// const CompactDatabaseDialog = observer( class CompactDatabaseDialog extends React.Component<{ database: InstrumentDatabase; }> { sizeBefore: number; sizeAfter: number | undefined; sizeReduced: number | undefined; constructor(props: any) { super(props); makeObservable(this, { sizeBefore: observable, sizeAfter: observable, sizeReduced: observable }); this.sizeBefore = fs.statSync(this.props.database.filePath).size; } async componentDidMount() { try { await dbVacuum(); runInAction(() => { this.props.database.timeOfLastDatabaseCompactOperation = Date.now(); }); runInAction(() => { var fs = require("fs"); this.sizeAfter = fs.statSync( this.props.database.filePath ).size; this.props.database.databaseSize = this.sizeAfter!; this.sizeReduced = (100 * (this.sizeBefore - this.sizeAfter!)) / this.sizeBefore; if (this.sizeReduced < 1) { this.sizeReduced = Math.round(100 * this.sizeReduced) / 100; } else if (this.sizeReduced < 10) { this.sizeReduced = Math.round(10 * this.sizeReduced) / 10; } else { this.sizeReduced = Math.round(this.sizeReduced); } }); } catch (err) { notification.error(err); } } render() { return ( {this.sizeReduced !== undefined && ( )}
Size before {formatBytes(this.sizeBefore)}
Size after {this.sizeAfter !== undefined ? ( formatBytes(this.sizeAfter) ) : ( )}
Size reduced by {formatBytes( this.sizeBefore - this.sizeAfter! )}{" "} or {this.sizeReduced}%
); } } ); //////////////////////////////////////////////////////////////////////////////// const DatabaseListItem = observer( class DbPathListItem extends React.Component<{ database: InstrumentDatabase; isSelected: boolean; onSelect: () => void; }> { render() { const { database, isSelected, onSelect } = this.props; const className = classNames({ selected: isSelected }); return ( {database.isActive ? "[ACTIVE] " : ""} {path.parse(database.filePath).name} ); } } ); //////////////////////////////////////////////////////////////////////////////// const SelectedDatabaseDetails = observer( class SelectedDatabaseDetails extends React.Component { render() { const selectedDatabase = settingsController.selectedDatabase; if (!selectedDatabase) { return null; } return (
{!selectedDatabase.isActive && (
)}
{selectedDatabase.filePath}
Database size is{" "} {formatBytes(selectedDatabase.databaseSize)}.
Database compacted{" "} {getMoment()( selectedDatabase.timeOfLastDatabaseCompactOperation ).fromNow()} .
{selectedDatabase.isCompactDatabaseAdvisable && (
{COMPACT_DATABASE_MESSAGE}
)}
); } } ); //////////////////////////////////////////////////////////////////////////////// const DatatabaseList = observer( class DatatabaseList extends React.Component { ref = React.createRef(); componentDidMount() { this.ensureSelectedVisible(); } componentDidUpdate() { this.ensureSelectedVisible(); } ensureSelectedVisible() { const selected = this.ref.current?.querySelector(".selected"); if (selected) { selected.scrollIntoView({ block: "nearest" }); } } render() { return (
{instrumentDatabases.databases.map( database => ( { settingsController.selectedDatabase = database; }) )} /> ) )}
); } } ); //////////////////////////////////////////////////////////////////////////////// const Databases = observer( class Databases extends React.Component { factory(node: FlexLayout.TabNode) { var component = node.getComponent(); if (component === "list") { return ; } if (component === "details") { return ; } return null; } render() { return ( Databases
); } } ); //////////////////////////////////////////////////////////////////////////////// const PythonSettings = observer( class PythonSettings extends React.Component { constructor(props: any) { super(props); const { PythonShell } = require("python-shell") as typeof import("python-shell"); PythonShell.runString( "import sys;print(sys.executable)", undefined, action((err, output) => { if (err) { console.log(err); this.pythonPathError = true; } else if (!output) { this.pythonPathError = true; } else { this.pythonPath = output[0]; } }) ); makeObservable(this, { pythonPath: observable, pythonPathError: observable }); } pythonPath: string = ""; pythonPathError: boolean = false; render() { return ( Python (settingsController.pythonUseCustomPath = value) )} checkboxStyleSwitch={true} /> {settingsController.pythonUseCustomPath && ( { settingsController.pythonCustomPath = value; })} /> )} ); } } ); //////////////////////////////////////////////////////////////////////////////// export const Settings = observer( class Settings extends React.Component { render() { return (
{Object.keys(LOCALES) .slice() .sort((a, b) => stringCompare( (LOCALES as any)[a], (LOCALES as any)[b] ) ) .map(locale => ( ))} {DATE_FORMATS.map(dateFormat => ( ))} {TIME_FORMATS.map(timeFormat => ( ))} {settingsController.restartRequired && (
)}
); } } ); ////////////////////////////////////////////////////////////////////////////////