Skip to content

Commit

Permalink
feat: toggle application theme manually (#460)
Browse files Browse the repository at this point in the history
Users can toggle the Console theme from the application menu.

Console theme can be changed by one of the following ways:
1. change the work station appearance (f.e: system settings ->
appearance -> light | dark | auto)
2. toggle the app theme from the application menu
3. use key shortcuts:
for light theme: ```"darwin" ? "Alt+Cmd+L" : "Alt+Shift+L"```
for dark theme: ```"darwin" ? "Alt+Cmd+D" : "Alt+Shift+D"```
for auto theme: ```"darwin" ? "Alt+Cmd+A" : "Alt+Shift+A"```

The console will persist the user manual selection both when the console
run as an electron application (using electron-store) and when running
in the browser (playground) using local-storage .

resolves: #2062
  • Loading branch information
ainvoner committed May 3, 2023
1 parent 7625940 commit df3c5c4
Show file tree
Hide file tree
Showing 71 changed files with 451 additions and 160 deletions.
2 changes: 1 addition & 1 deletion console/app/e2e/sanity.test.ts
Expand Up @@ -81,7 +81,7 @@ test("map view", async () => {
const window = await electronApp.firstWindow();
window.on("console", console.log);
await window.setViewportSize({ width: 1920, height: 1080 });

await pause(5000);
const mapView = window.getByTestId("map-view");
expect(await mapView.screenshot()).toMatchSnapshot("map-view.png", {
maxDiffPixelRatio: 0.3,
Expand Down
Binary file modified console/app/e2e/sanity.test.ts-snapshots/explorer-tree-menu.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified console/app/e2e/sanity.test.ts-snapshots/map-view.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 161 additions & 0 deletions console/app/electron/main/appMenu.ts
@@ -0,0 +1,161 @@
import { Updater } from "@wingconsole/server";
import {
BrowserWindow,
dialog,
Menu,
nativeImage,
shell,
MenuItem,
} from "electron";

import { ThemeMode } from "./config.js";

let defaultMenuItems: MenuItem[];

export const initApplicationMenu = () => {
// The default menu from Electron come with very useful items.
// File menu, which is the second entry on the list and Help menu (which is the last) are being modified
defaultMenuItems = Menu.getApplicationMenu()?.items!;
// remove default Help menu
defaultMenuItems?.pop();
};

export const setApplicationMenu = (options: {
publicPath: string;
onFileOpen: (file: string) => void;
onThemeModeChange: (themeMode: ThemeMode) => void;
updater: Updater;
themeMode: ThemeMode;
}) => {
const menuTemplateArray = [
{
...defaultMenuItems[0]!,
submenu: defaultMenuItems[0]!.submenu!.items.map((item) => ({
...item,
label: item.label.replace("@wingconsole/app", "Wing Console"),
})) as any,
},
{
label: "File",
submenu: [
{
label: "Open",
accelerator: "Command+O",
async click() {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Wing File", extensions: ["w"] }],
});
if (canceled) {
return;
}

const [wingfile] = filePaths;
if (wingfile) {
options.onFileOpen(wingfile);
}
},
},
{ type: "separator" },
{
label: "Close Window",
accelerator: "Command+W",
click() {
BrowserWindow.getFocusedWindow()?.close();
},
},
],
},
{
...defaultMenuItems[2]!,
},
{
...defaultMenuItems[3]!,
submenu: [
...defaultMenuItems[3]!.submenu!.items.filter(
(item) => item.label !== "Toggle Developer Tools",
),
{ type: "separator" },
{
type: "submenu",
label: "Theme",
submenu: [
{
label: "Light Theme",
type: "checkbox",
checked: options.themeMode === "light",
accelerator:
process.platform === "darwin" ? "Alt+Cmd+L" : "Alt+Shift+L",
click: async () => {
options.onThemeModeChange("light");
},
},
{
label: "Dark Theme",
type: "checkbox",
checked: options.themeMode === "dark",
accelerator:
process.platform === "darwin" ? "Alt+Cmd+D" : "Alt+Shift+D",
click: async () => {
options.onThemeModeChange("dark");
},
},
{
label: "System Appearance",
type: "checkbox",
checked: options.themeMode === "auto",
accelerator:
process.platform === "darwin" ? "Alt+Cmd+A" : "Alt+Shift+A",
click: async () => {
options.onThemeModeChange("auto");
},
},
],
},
],
},
...defaultMenuItems.slice(4),
{
role: "help",
submenu: [
{
label: "Learn More",
click: async () => {
await shell.openExternal("https://winglang.io");
},
},
{
label: "Documentation",
click: async () => {
await shell.openExternal("https://docs.winglang.io");
},
},
{
label: "Open an Issue",
click: async () => {
await shell.openExternal(
"https://github.com/winglang/wing/issues/new/choose",
);
},
},
{
label: "Check for Updates",
enabled: ["initial", "update-not-available", "error"].includes(
options.updater.status().status,
),
click: async () => {
await options.updater.checkForUpdates();
},
},
],
},
];

if (process.platform !== "darwin") {
// remove the first menu item on windows and linux
menuTemplateArray.shift();
}

const appMenu = Menu.buildFromTemplate(menuTemplateArray as any);
Menu.setApplicationMenu(appMenu);
};
26 changes: 26 additions & 0 deletions console/app/electron/main/config.ts
@@ -0,0 +1,26 @@
import { EventEmitter } from "node:events";

import { Config } from "@wingconsole/server";
export type ThemeMode = "light" | "dark" | "auto";
export class AppConfig extends EventEmitter implements Config {
config: Record<string, unknown>;
constructor() {
super();
this.config = {
themeMode: "auto",
};
}
set(key: string, value: unknown): void {
this.config[key] = value;
this.emit("config-change");
}
get<T>(key: string): T {
return this.config[key] as T;
}
addEventListener(event: "config-change", listener: () => void): void {
this.addListener(event, listener);
}
removeEventListener(event: "config-change", listener: () => void): void {
this.removeListener(event, listener);
}
}
139 changes: 46 additions & 93 deletions console/app/electron/main/index.ts
Expand Up @@ -2,17 +2,25 @@ import path from "node:path";

import { createConsoleServer } from "@wingconsole/server";
import { config } from "dotenv";
import { app, BrowserWindow, dialog, Menu, screen, shell } from "electron";
import { app, BrowserWindow, dialog, screen, nativeTheme } from "electron";
import log from "electron-log";
import fixPath from "fix-path";

import { initApplicationMenu, setApplicationMenu } from "./appMenu.js";
import { AppConfig, ThemeMode } from "./config.js";
import { WING_PROTOCOL_SCHEME } from "./protocol.js";
import { SegmentAnalytics } from "./segmentAnalytics.js";
import { ThemeStore } from "./themStore.js";
import { updater } from "./updater.js";

config();
fixPath();

const appConfig = new AppConfig();
const themeStore = new ThemeStore();
// set initial theme mode
appConfig.set("themeMode", themeStore.getThemeMode());

log.info("Application entrypoint");

log.transports.console.bind(process.stdout);
Expand Down Expand Up @@ -46,6 +54,7 @@ async function createWindow(options: { title?: string; port: number }) {
webPreferences: {
devTools: import.meta.env.DEV,
},
titleBarStyle: "hidden",
});

if (import.meta.env.DEV) {
Expand All @@ -55,7 +64,11 @@ async function createWindow(options: { title?: string; port: number }) {
`${import.meta.env.BASE_URL}?port=${options.port}`,
);
void window
.loadURL(`${import.meta.env.BASE_URL}?port=${options.port}`)
.loadURL(
`${import.meta.env.BASE_URL}?port=${options.port}&title=${
options.title ?? "Wing Console"
}`,
)
.then(() => {
if (
!process.env.PLAYWRIGHT_TEST &&
Expand All @@ -71,6 +84,7 @@ async function createWindow(options: { title?: string; port: number }) {
void window.loadFile(path.join(ROOT_PATH.dist, "index.html"), {
query: {
port: options.port.toString(),
title: options.title ?? "Wing Console",
},
});
}
Expand Down Expand Up @@ -115,6 +129,7 @@ function createWindowManager() {
wingfile,
log,
updater,
config: appConfig,
});

newWindow = await createWindow({
Expand Down Expand Up @@ -266,103 +281,41 @@ async function main() {
await app.whenReady();
log.info("app is ready");

// The default menu from Electron come with very useful items.
// File menu, which is the second entry on the list and Help menu (which is the last) are being modified
const defaultMenuItems = Menu.getApplicationMenu()?.items!;
// remove default Help menu
defaultMenuItems.pop();

const checkForUpdatesItemId = "check-for-updates-menu-item";
const menuTemplateArray = [
{
...defaultMenuItems[0]!,
submenu: defaultMenuItems[0]!.submenu!.items.map((item) => ({
...item,
label: item.label.replace("@wingconsole/app", "Wing Console"),
})) as any,
},
{
label: "File",
submenu: [
{
label: "Open",
accelerator: "Command+O",
async click() {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Wing File", extensions: ["w"] }],
});
if (canceled) {
return;
}

const [wingfile] = filePaths;
if (wingfile) {
void windowManager.open(wingfile);
}
},
},
{ type: "separator" },
{
label: "Close Window",
accelerator: "Command+W",
click() {
BrowserWindow.getFocusedWindow()?.close();
},
},
],
const getThemeMode = (): ThemeMode => {
return appConfig.get<ThemeMode>("themeMode");
};

const setThemeMode = (themeMode: ThemeMode) => {
themeStore.setThemeMode(themeMode);
appConfig.set("themeMode", themeMode);
};

const getApplicationMenuOptions = () => ({
themeMode: getThemeMode(),
onFileOpen: (wingfile: string) => {
void windowManager.open(wingfile);
},
...defaultMenuItems.slice(2),
{
role: "help",
submenu: [
{
label: "Learn More",
click: async () => {
await shell.openExternal("https://winglang.io");
},
},
{
label: "Documentation",
click: async () => {
await shell.openExternal("https://docs.winglang.io");
},
},
{
label: "Open an Issue",
click: async () => {
await shell.openExternal(
"https://github.com/winglang/wing/issues/new/choose",
);
},
},
{
label: "Check for Updates",
id: checkForUpdatesItemId,
enabled: false,
click: async () => {
await updater.checkForUpdates();
},
},
],
updater,
onThemeModeChange: (themeMode: ThemeMode) => {
setThemeMode(themeMode);
},
];

if (process.platform !== "darwin") {
// remove the first menu item on windows and linux
menuTemplateArray.shift();
}
publicPath: ROOT_PATH.public,
});

const appMenu = Menu.buildFromTemplate(menuTemplateArray as any);
Menu.setApplicationMenu(appMenu);
initApplicationMenu();
setApplicationMenu(getApplicationMenuOptions());

// update check for updates menu item enabled state
updater.addEventListener("status-change", () => {
appMenu.getMenuItemById(checkForUpdatesItemId)!.enabled = [
"initial",
"update-not-available",
"error",
].includes(updater.status().status);
setApplicationMenu(getApplicationMenuOptions());
});

appConfig.addEventListener("config-change", () => {
setApplicationMenu(getApplicationMenuOptions());
});

nativeTheme.on("updated", () => {
setApplicationMenu(getApplicationMenuOptions());
});

if (import.meta.env.DEV || process.env.PLAYWRIGHT_TEST || process.env.CI) {
Expand Down

0 comments on commit df3c5c4

Please sign in to comment.