diff --git a/github b/github index 9c3cc6d..7c087d4 160000 --- a/github +++ b/github @@ -1 +1 @@ -Subproject commit 9c3cc6d05838a03a5827836b300f8bc8e71b26d2 +Subproject commit 7c087d46ad75ff641d5862a57ff13f3e860cc8a4 diff --git a/jenkins/create_build.sh b/jenkins/create_build.sh index d96f108..08ef2e3 100755 --- a/jenkins/create_build.sh +++ b/jenkins/create_build.sh @@ -122,5 +122,5 @@ function deploy_client() { #install_npm #compile_scripts #compile_native -package_client -#deploy_client +#package_client +deploy_client diff --git a/modules/core/main-window/index.ts b/modules/core/main-window/index.ts index 57b6042..9fefcbc 100644 --- a/modules/core/main-window/index.ts +++ b/modules/core/main-window/index.ts @@ -10,6 +10,7 @@ import * as loader from "./../ui-loader"; import * as url from "url"; import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window"; import {referenceApp, dereferenceApp} from "../AppInstance"; +import {closeURLPreview, openURLPreview} from "../url-preview"; // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. @@ -49,7 +50,7 @@ function spawnMainWindow(rendererEntryPoint: string) { mainWindow.on('closed', () => { app.releaseSingleInstanceLock(); - require("../url-preview").close(); + closeURLPreview(); mainWindow = null; dereferenceApp(); @@ -94,8 +95,7 @@ function spawnMainWindow(rendererEntryPoint: string) { } console.log("Got new window " + frameName); - const url_preview = require("./url-preview"); - url_preview.open_preview(url_str); + openURLPreview(url_str).then(() => {}); } catch(error) { console.error("Failed to open preview window for URL %s: %o", url_str, error); dialog.showErrorBox("Failed to open preview", "Failed to open preview URL: " + url_str + "\nError: " + error); diff --git a/modules/core/render-backend/ExternalModal.ts b/modules/core/render-backend/ExternalModal.ts index a202058..522c001 100644 --- a/modules/core/render-backend/ExternalModal.ts +++ b/modules/core/render-backend/ExternalModal.ts @@ -4,7 +4,7 @@ import {ProxiedClass} from "../../shared/proxy/Definitions"; import {BrowserWindow, dialog} from "electron"; import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window"; import {Arguments, processArguments} from "../../shared/process-arguments"; -import {open_preview} from "../url-preview"; +import {openURLPreview} from "../url-preview"; import * as path from "path"; class ProxyImplementation extends ProxiedClass implements ExternalModal { @@ -71,15 +71,16 @@ class ProxyImplementation extends ProxiedClass implements Externa } } - open_preview(url.toString()); + openURLPreview(url.toString()); } catch(error) { console.error("Failed to open preview window for URL %s: %o", url_str, error); dialog.showErrorBox("Failed to open preview", "Failed to open preview URL: " + url_str + "\nError: " + error); } }); - if(processArguments.has_flag(Arguments.DEV_TOOLS)) + if(processArguments.has_flag(Arguments.DEV_TOOLS)) { this.windowInstance.webContents.openDevTools(); + } try { await this.windowInstance.loadURL(url); diff --git a/modules/core/render-backend/menu.ts b/modules/core/render-backend/menu.ts index 407aa32..04f6bbf 100644 --- a/modules/core/render-backend/menu.ts +++ b/modules/core/render-backend/menu.ts @@ -1,34 +1,26 @@ import * as electron from "electron"; import ipcMain = electron.ipcMain; import BrowserWindow = electron.BrowserWindow; +import {NativeMenuBarEntry} from "../../shared/MenuBarDefinitions"; +import {Menu, MenuItemConstructorOptions} from "electron"; -ipcMain.on('top-menu', (event, menu_template: electron.MenuItemConstructorOptions[]) => { +ipcMain.on("menu-bar", (event, menuBar: NativeMenuBarEntry[]) => { const window = BrowserWindow.fromWebContents(event.sender); - const process_template = (item: electron.MenuItemConstructorOptions) => { - if(typeof(item.icon) === "string" && item.icon.startsWith("data:")) - item.icon = electron.nativeImage.createFromDataURL(item.icon); - - item.click = () => window.webContents.send('top-menu', item.id); - for(const i of item.submenu as electron.MenuItemConstructorOptions[] || []) { - process_template(i); - } - }; - - for(const m of menu_template) - process_template(m); - try { - const menu = new electron.Menu(); - for(const m of menu_template) { - try { - menu.append(new electron.MenuItem(m)); - } catch(error) { - console.error("Failed to build menu entry: %o\nSource: %o", error, m); + const processEntry = (entry: NativeMenuBarEntry): MenuItemConstructorOptions => { + return { + type: entry.type === "separator" ? "separator" : entry.children?.length ? "submenu" : "normal", + label: entry.label, + icon: entry.icon ? electron.nativeImage.createFromDataURL(entry.icon).resize({ height: 16, width: 16 }) : undefined, + enabled: !entry.disabled, + click: entry.uniqueId && (() => event.sender.send("menu-bar", "item-click", entry.uniqueId)), + submenu: entry.children?.map(processEntry) } - } - window.setMenu(menu_template.length == 0 ? undefined : menu); - } catch(error) { - console.error("Failed to set window menu: %o", error); + }; + + window.setMenu(Menu.buildFromTemplate(menuBar.map(processEntry))); + } catch (error) { + console.error("failed to set menu bar for %s: %o", window.getTitle(), error); } }); \ No newline at end of file diff --git a/modules/core/url-preview/index.ts b/modules/core/url-preview/index.ts index fe8d0a1..fc82557 100644 --- a/modules/core/url-preview/index.ts +++ b/modules/core/url-preview/index.ts @@ -5,7 +5,7 @@ import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window"; let global_window: electron.BrowserWindow; let global_window_promise: Promise; -export async function close() { +export async function closeURLPreview() { while(global_window_promise) { try { await global_window_promise; @@ -20,13 +20,14 @@ export async function close() { } } -export async function open_preview(url: string) { +export async function openURLPreview(url: string) { while(global_window_promise) { try { await global_window_promise; break; } catch(error) {} /* error will be already logged */ } + if(!global_window) { global_window_promise = (async () => { global_window = new electron.BrowserWindow({ diff --git a/modules/renderer/ContextMenu.ts b/modules/renderer/ContextMenu.ts index ed000a7..6d2c4ab 100644 --- a/modules/renderer/ContextMenu.ts +++ b/modules/renderer/ContextMenu.ts @@ -1,64 +1,123 @@ import {ContextMenuEntry, ContextMenuFactory, setGlobalContextMenuFactory} from "tc-shared/ui/ContextMenu"; import * as electron from "electron"; import {MenuItemConstructorOptions} from "electron"; -import {clientIconClassToImage} from "./IconHelper"; +import {clientIconClassToImage, remoteIconDatafier, RemoteIconWrapper} from "./IconHelper"; +import {getIconManager, RemoteIconInfo} from "tc-shared/file/Icons"; const {Menu} = electron.remote; -let currentMenu: electron.Menu; +let currentMenu: ContextMenuInstance; +class ContextMenuInstance { + private readonly closeCallback: () => void | undefined; + private readonly menuOptions: MenuItemConstructorOptions[]; + private currentMenu: electron.Menu; -function mapMenuEntry(entry: ContextMenuEntry) : MenuItemConstructorOptions { - switch (entry.type) { - case "normal": - return { - type: "normal", - label: typeof entry.label === "string" ? entry.label : entry.label.text, - enabled: entry.enabled, - visible: entry.visible, - click: entry.click, - icon: typeof entry.icon === "string" ? clientIconClassToImage(entry.icon) : undefined, - id: entry.uniqueId, - submenu: entry.subMenu ? entry.subMenu.map(mapMenuEntry).filter(e => !!e) : undefined - }; + private wrappedIcons: RemoteIconWrapper[] = []; + private wrappedIconListeners: (() => void)[] = []; - case "checkbox": - return { - type: "normal", - label: typeof entry.label === "string" ? entry.label : entry.label.text, - enabled: entry.enabled, - visible: entry.visible, - click: entry.click, - id: entry.uniqueId, + constructor(entries: ContextMenuEntry[], closeCallback: () => void | undefined) { + this.closeCallback = closeCallback; + this.menuOptions = entries.map(e => this.wrapEntry(e)).filter(e => !!e); + } - checked: entry.checked - }; + destroy() { + this.currentMenu?.closePopup(); + this.currentMenu = undefined; - case "separator": - return { - type: "separator" - }; + this.wrappedIconListeners.forEach(callback => callback()); + this.wrappedIcons.forEach(icon => remoteIconDatafier.unrefIcon(icon)); - default: - return undefined; + this.wrappedIcons = []; + this.wrappedIconListeners = []; + } + + spawn(pageX: number, pageY: number) { + this.currentMenu = Menu.buildFromTemplate(this.menuOptions); + this.currentMenu.popup({ + callback: () => { + if(this.closeCallback) { + this.closeCallback(); + } + currentMenu = undefined; + }, + x: pageX, + y: pageY, + window: electron.remote.BrowserWindow.getFocusedWindow() + }); + } + + private wrapEntry(entry: ContextMenuEntry) : MenuItemConstructorOptions { + if(typeof entry.visible === "boolean" && !entry.visible) { return undefined; } + + let options: MenuItemConstructorOptions; + let icon: string | RemoteIconInfo | undefined; + + switch (entry.type) { + case "normal": + icon = entry.icon; + options = { + type: entry.subMenu?.length ? "submenu" : "normal", + label: typeof entry.label === "string" ? entry.label : entry.label.text, + enabled: entry.enabled, + click: entry.click, + id: entry.uniqueId, + submenu: entry.subMenu?.length ? entry.subMenu.map(e => this.wrapEntry(e)).filter(e => !!e) : undefined + }; + break; + + case "checkbox": + icon = entry.icon; + options = { + type: "checkbox", + label: typeof entry.label === "string" ? entry.label : entry.label.text, + enabled: entry.enabled, + click: entry.click, + id: entry.uniqueId, + checked: entry.checked + }; + break; + + case "separator": + return { + type: "separator" + }; + + default: + return undefined; + } + + if(typeof icon === "object") { + const remoteIcon = getIconManager().resolveIcon(icon.iconId, icon.serverUniqueId, icon.handlerId); + const wrapped = remoteIconDatafier.resolveIcon(remoteIcon); + remoteIconDatafier.unrefIcon(wrapped); + + /* + // Sadly we can't update the icon on the fly, so we've to live with whatever we have + this.wrappedIcons.push(wrapped); + this.wrappedIconListeners.push(wrapped.onDataUrlChange(dataUrl => { + options.icon = electron.nativeImage.createFromDataURL(dataUrl); + })); + */ + + if(wrapped.getDataUrl()) { + options.icon = electron.remote.nativeImage.createFromDataURL(wrapped.getDataUrl()); + } + } else if(typeof icon === "string") { + options.icon = clientIconClassToImage(icon); + } + + return options; } } setGlobalContextMenuFactory(new class implements ContextMenuFactory { closeContextMenu() { - currentMenu?.closePopup(); + currentMenu?.destroy(); currentMenu = undefined; } spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[], callbackClose?: () => void) { this.closeContextMenu(); - currentMenu = Menu.buildFromTemplate(entries.map(mapMenuEntry).filter(e => !!e)); - currentMenu.popup({ - callback: () => { - callbackClose(); - currentMenu = undefined; - }, - x: position.pageX, - y: position.pageY, - window: electron.remote.BrowserWindow.getFocusedWindow() - }); + currentMenu = new ContextMenuInstance(entries, callbackClose); + currentMenu.spawn(position.pageX, position.pageY); } }); \ No newline at end of file diff --git a/modules/renderer/IconHelper.ts b/modules/renderer/IconHelper.ts index f491b57..6459510 100644 --- a/modules/renderer/IconHelper.ts +++ b/modules/renderer/IconHelper.ts @@ -6,9 +6,10 @@ import { spriteUrl as kClientSpriteUrl, spriteWidth as kClientSpriteWidth, spriteHeight as kClientSpriteHeight, - spriteEntries as kClientSpriteEntries + spriteEntries as kClientSpriteEntries, ClientIcon } from "svg-sprites/client-icons"; import {NativeImage} from "electron"; +import {RemoteIcon} from "tc-shared/file/Icons"; let nativeSprite: NativeImage; @@ -24,6 +25,134 @@ export function clientIconClassToImage(klass: string) : NativeImage { }); } +export class RemoteIconDatafier { + private cachedIcons: {[key: string]:{ refCount: number, icon: RemoteIconWrapper }} = {}; + private cleanupTimer; + + constructor() { } + + destroy() { + clearTimeout(this.cleanupTimer); + } + + resolveIcon(icon: RemoteIcon) : RemoteIconWrapper { + const uniqueId = icon.iconId + "-" + icon.serverUniqueId; + if(!this.cachedIcons[uniqueId]) { + this.cachedIcons[uniqueId] = { + refCount: 0, + icon: new RemoteIconWrapper(uniqueId, icon) + } + } + + const cache = this.cachedIcons[uniqueId]; + cache.refCount++; + return cache.icon; + } + + unrefIcon(icon: RemoteIconWrapper) { + const cache = this.cachedIcons[icon.uniqueId]; + if(!cache) { return; } + + cache.refCount--; + if(cache.refCount <= 0) { + if(this.cleanupTimer) { + clearTimeout(this.cleanupTimer); + } + this.cleanupTimer = setTimeout(() => this.cleanupIcons(), 10 * 1000); + } + } + + private cleanupIcons() { + this.cleanupTimer = undefined; + for(const key of Object.keys(this.cachedIcons)) { + if(this.cachedIcons[key].refCount <= 0) { + this.cachedIcons[key].icon.destroy(); + delete this.cachedIcons[key]; + } + } + } +} +export const remoteIconDatafier = new RemoteIconDatafier(); + +export class RemoteIconWrapper { + readonly callbackUpdated: ((newUrl: string) => void)[] = []; + readonly uniqueId: string; + + private readonly icon: RemoteIcon; + private readonly callbackStateChanged: () => void; + private dataUrl: string | undefined; + private currentImageUrl: string; + + constructor(uniqueId: string, icon: RemoteIcon) { + this.icon = icon; + this.uniqueId = uniqueId; + this.callbackStateChanged = this.handleIconStateChanged.bind(this); + + this.icon.events.on("notify_state_changed", this.callbackStateChanged); + this.handleIconStateChanged(); + } + + destroy() { + this.icon.events.off("notify_state_changed", this.callbackStateChanged); + this.currentImageUrl = undefined; + } + + getDataUrl() : string | undefined { return this.dataUrl; } + + onDataUrlChange(callback: (newUrl: string) => void) : () => void { + this.callbackUpdated.push(callback); + return () => { + const index = this.callbackUpdated.indexOf(callback); + if(index !== -1) { this.callbackUpdated.splice(index, 1); } + } + } + + private handleIconStateChanged() { + if(this.icon.getState() === "loaded") { + const imageUrl = this.icon.getImageUrl(); + this.currentImageUrl = this.icon.getImageUrl(); + + const image = new Image(); + image.src = imageUrl; + + new Promise((resolve, reject) => { + image.onload = resolve; + image.onerror = reject; + }).then(() => { + if(this.currentImageUrl !== imageUrl) { return; } + + const canvas = document.createElement("canvas"); + if(image.naturalWidth > 1000 || image.naturalHeight > 1000) { + throw "image dimensions are too large"; + } else { + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + } + + canvas.getContext("2d").drawImage(image, 0, 0); + + this.setDataUrl(imageUrl, canvas.toDataURL()); + }).catch(() => { + this.setDataUrl(imageUrl, clientIconClassToImage(ClientIcon.Error).toDataURL()); + }); + } else if(this.icon.getState() === "error") { + this.setDataUrl(undefined, clientIconClassToImage(ClientIcon.Error).toDataURL()); + } else { + this.setDataUrl(undefined, undefined); + } + } + + private setDataUrl(sourceImageUrl: string | undefined, dataUrl: string) { + if(sourceImageUrl && this.currentImageUrl !== sourceImageUrl) { return; } + this.currentImageUrl = undefined; /* no image is loading any more */ + + if(this.dataUrl === dataUrl) { return; } + + this.dataUrl = dataUrl; + this.callbackUpdated.forEach(callback => callback(dataUrl)); + } +} + loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { priority: 100, name: "native icon sprite loader", @@ -40,7 +169,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { canvas.height = kClientSpriteHeight; canvas.getContext("2d").drawImage(image, 0, 0); - nativeSprite = electron.remote.nativeImage.createFromDataURL( canvas.toDataURL()); + nativeSprite = electron.remote.nativeImage.createFromDataURL(canvas.toDataURL()); } }) diff --git a/modules/renderer/MenuBar.ts b/modules/renderer/MenuBar.ts new file mode 100644 index 0000000..d423d70 --- /dev/null +++ b/modules/renderer/MenuBar.ts @@ -0,0 +1,99 @@ +import * as electron from "electron"; +import {MenuBarDriver, MenuBarEntry} from "tc-shared/ui/frames/menu-bar"; +import {IpcRendererEvent} from "electron"; +import {getIconManager} from "tc-shared/file/Icons"; +import {clientIconClassToImage, remoteIconDatafier, RemoteIconWrapper} from "./IconHelper"; +import {NativeMenuBarEntry} from "../shared/MenuBarDefinitions"; + +let uniqueEntryIdIndex = 0; +export class NativeMenuBarDriver implements MenuBarDriver { + private readonly ipcChannelListener; + + private menuEntries: NativeMenuBarEntry[] = []; + private remoteIconReferences: RemoteIconWrapper[] = []; + private remoteIconListeners: (() => void)[] = []; + private callbacks: {[key: string]: () => void} = {}; + + constructor() { + this.ipcChannelListener = this.handleMenuBarEvent.bind(this); + + electron.ipcRenderer.on("menu-bar", this.ipcChannelListener); + } + + destroy() { + electron.ipcRenderer.off("menu-bar", this.ipcChannelListener); + this.internalClearEntries(); + } + + private internalClearEntries() { + this.callbacks = {}; + this.menuEntries = []; + + this.remoteIconListeners.forEach(callback => callback()); + this.remoteIconListeners = []; + + this.remoteIconReferences.forEach(icon => remoteIconDatafier.unrefIcon(icon)); + this.remoteIconReferences = []; + } + + clearEntries() { + this.internalClearEntries() + electron.ipcRenderer.send("menu-bar", []); + } + + setEntries(entries: MenuBarEntry[]) { + this.internalClearEntries(); + this.menuEntries = entries.map(e => this.wrapEntry(e)).filter(e => !!e); + electron.ipcRenderer.send("menu-bar", this.menuEntries); + } + + private wrapEntry(entry: MenuBarEntry) : NativeMenuBarEntry { + if(entry.type === "separator") { + return { type: "separator", uniqueId: entry.uniqueId || "item-" + (++uniqueEntryIdIndex) }; + } else if(entry.type === "normal") { + if(typeof entry.visible === "boolean" && !entry.visible) { + return null; + } + + let result = { + type: "normal", + uniqueId: entry.uniqueId || "item-" + (++uniqueEntryIdIndex), + label: entry.label, + disabled: entry.disabled, + children: entry.children?.map(e => this.wrapEntry(e)).filter(e => !!e) + } as NativeMenuBarEntry; + + if(entry.click) { + this.callbacks[result.uniqueId] = entry.click; + } + + if(typeof entry.icon === "object") { + /* we've a remote icon */ + const remoteIcon = getIconManager().resolveIcon(entry.icon.iconId, entry.icon.serverUniqueId, entry.icon.handlerId); + const wrapped = remoteIconDatafier.resolveIcon(remoteIcon); + this.remoteIconReferences.push(wrapped); + + wrapped.onDataUrlChange(url => { + result.icon = url; + electron.ipcRenderer.send("menu-bar", this.menuEntries); + }); + result.icon = wrapped.getDataUrl(); + } else if(typeof entry.icon === "string") { + result.icon = clientIconClassToImage(entry.icon).toDataURL(); + } + + return result; + } else { + return undefined; + } + } + + private handleMenuBarEvent(_event: IpcRendererEvent, eventType: string, ...args) { + if(eventType === "item-click") { + const callback = this.callbacks[args[0]]; + if(typeof callback === "function") { + callback(); + } + } + } +} \ No newline at end of file diff --git a/modules/renderer/MenuBarHandler.ts b/modules/renderer/MenuBarHandler.ts deleted file mode 100644 index 5c23550..0000000 --- a/modules/renderer/MenuBarHandler.ts +++ /dev/null @@ -1,246 +0,0 @@ -import {clientIconClassToImage} from "./IconHelper"; -import * as electron from "electron"; -import * as mbar from "tc-shared/ui/frames/MenuBar"; -import {Arguments, processArguments} from "../shared/process-arguments"; - -import ipcRenderer = electron.ipcRenderer; -import {LocalIcon} from "tc-shared/file/Icons"; -namespace native { - import ipcRenderer = electron.ipcRenderer; - let _item_index = 1; - - abstract class NativeMenuBase { - protected _handle: NativeMenuBar; - protected _click: () => any; - id: string; - - protected constructor(handle: NativeMenuBar, id?: string) { - this._handle = handle; - this.id = id || ("item_" + (_item_index++)); - } - - abstract build() : electron.MenuItemConstructorOptions; - abstract items(): (mbar.MenuItem | mbar.HRItem)[]; - - trigger_click() { - if(this._click) - this._click(); - } - } - - class NativeMenuItem extends NativeMenuBase implements mbar.MenuItem { - private _items: (NativeMenuItem | NativeHrItem)[] = []; - private _label: string; - private _enabled: boolean = true; - private _visible: boolean = true; - - private _icon_data: string; - - constructor(handle: NativeMenuBar) { - super(handle); - - } - - append_hr(): mbar.HRItem { - const item = new NativeHrItem(this._handle); - this._items.push(item); - return item; - } - - append_item(label: string): mbar.MenuItem { - const item = new NativeMenuItem(this._handle); - item.label(label); - this._items.push(item); - return item; - } - - click(callback: () => any): this { - this._click = callback; - return this; - } - - delete_item(item: mbar.MenuItem | mbar.HRItem) { - const i_index = this._items.indexOf(item as any); - if(i_index < 0) return; - this._items.splice(i_index, 1); - } - - disabled(value?: boolean): boolean { - if(typeof(value) === "boolean") - this._enabled = !value; - return !this._enabled; - } - - icon(klass?: string | Promise | LocalIcon): string { - if(typeof(klass) === "string") { - const buffer = clientIconClassToImage(klass); - if(buffer) - this._icon_data = buffer.toDataURL(); - } - return ""; - } - - items(): (mbar.MenuItem | mbar.HRItem)[] { - return this._items; - } - - label(value?: string): string { - if(typeof(value) === "string") - this._label = value; - return this._label; - } - - visible(value?: boolean): boolean { - if(typeof(value) === "boolean") - this._visible = value; - return this._visible; - } - - build(): Electron.MenuItemConstructorOptions { - return { - id: this.id, - - label: this._label || "", - - submenu: this._items.length > 0 ? this._items.map(e => e.build()) : undefined, - enabled: this._enabled, - visible: this._visible, - - icon: this._icon_data - } - } - } - - class NativeHrItem extends NativeMenuBase implements mbar.HRItem { - constructor(handle: NativeMenuBar) { - super(handle); - } - - build(): Electron.MenuItemConstructorOptions { - return { - type: 'separator', - id: this.id - } - } - - items(): (mbar.MenuItem | mbar.HRItem)[] { - return []; - } - } - - function is_similar_deep(a, b) { - if(typeof(a) !== typeof(b)) - return false; - if(typeof(a) !== "object") - return a === b; - - const aProps = Object.keys(a); - const bProps = Object.keys(b); - - if (aProps.length != bProps.length) - return false; - - for (let i = 0; i < aProps.length; i++) { - const propName = aProps[i]; - - if(!is_similar_deep(a[propName], b[propName])) - return false; - } - - return true; - } - - - export class NativeMenuBar implements mbar.MenuBarDriver { - private static _instance: NativeMenuBar; - - private menu: electron.Menu; - private _items: NativeMenuItem[] = []; - private _current_menu: electron.MenuItemConstructorOptions[]; - - public static instance() : NativeMenuBar { - if(!this._instance) - this._instance = new NativeMenuBar(); - return this._instance; - } - - append_item(label: string): mbar.MenuItem { - const item = new NativeMenuItem(this); - item.label(label); - this._items.push(item); - return item; - } - - delete_item(item: mbar.MenuItem) { - const i_index = this._items.indexOf(item as any); - if(i_index < 0) return; - this._items.splice(i_index, 1); - } - - flush_changes() { - const target_menu = this.build_menu(); - if(is_similar_deep(target_menu, this._current_menu)) - return; - - this._current_menu = target_menu; - ipcRenderer.send('top-menu', target_menu); - } - - private build_menu() : electron.MenuItemConstructorOptions[] { - return this._items.map(e => e.build()); - } - - items(): mbar.MenuItem[] { - return this._items; - } - - initialize() { - this.menu = new electron.remote.Menu(); - ipcRenderer.on('top-menu', (event, clicked_item) => { - console.log("Item %o clicked", clicked_item); - const check_item = (item: NativeMenuBase) => { - if(item.id == clicked_item) { - item.trigger_click(); - return true; - } - for(const child of item.items()) - if(check_item(child as NativeMenuBase)) - return true; - }; - - for(const item of this._items) - if(check_item(item)) - return; - }); - } - } -} - -mbar.set_driver(native.NativeMenuBar.instance()); -// @ts-ignore -mbar.native_actions = { - open_change_log() { - call_basic_action("open-changelog"); - }, - - check_native_update() { - call_basic_action("check-native-update"); - }, - - quit() { - call_basic_action("quit"); - }, - - open_dev_tools() { - call_basic_action("open-dev-tools"); - }, - - reload_page() { - call_basic_action("reload-window") - }, - - show_dev_tools() { return processArguments.has_flag(Arguments.DEV_TOOLS); } -}; - - -const call_basic_action = (name: string, ...args: any[]) => ipcRenderer.send('basic-action', name, ...args); \ No newline at end of file diff --git a/modules/renderer/audio/AudioRecorder.ts b/modules/renderer/audio/AudioRecorder.ts index b0eb294..0a2de80 100644 --- a/modules/renderer/audio/AudioRecorder.ts +++ b/modules/renderer/audio/AudioRecorder.ts @@ -16,6 +16,7 @@ import {NativeFilter, NStateFilter, NThresholdFilter, NVoiceLevelFilter} from ". import {IDevice} from "tc-shared/audio/recorder"; import {LogCategory, logWarn} from "tc-shared/log"; import NativeFilterMode = audio.record.FilterMode; +import {Settings, settings} from "tc-shared/settings"; export class NativeInput implements AbstractInput { static readonly instances = [] as NativeInput[]; @@ -37,8 +38,7 @@ export class NativeInput implements AbstractInput { this.nativeHandle = audio.record.create_recorder(); this.nativeConsumer = this.nativeHandle.create_consumer(); - this.nativeConsumer.toggle_rnnoise(true); - (window as any).consumer = this.nativeConsumer; /* FIXME! */ + this.nativeConsumer.toggle_rnnoise(settings.static_global(Settings.KEY_RNNOISE_FILTER)); this.nativeConsumer.callback_ended = () => { this.filtered = true; @@ -245,7 +245,7 @@ export class NativeLevelMeter implements LevelMeter { this.nativeRecorder = audio.record.create_recorder(); this.nativeConsumer = this.nativeRecorder.create_consumer(); - this.nativeConsumer.toggle_rnnoise(true); /* FIXME! */ + this.nativeConsumer.toggle_rnnoise(settings.static_global(Settings.KEY_RNNOISE_FILTER)); this.nativeFilter = this.nativeConsumer.create_filter_threshold(.5); this.nativeFilter.set_attack_smooth(.75); diff --git a/modules/renderer/backend-impl/Backend.ts b/modules/renderer/backend-impl/Backend.ts new file mode 100644 index 0000000..d3dcf36 --- /dev/null +++ b/modules/renderer/backend-impl/Backend.ts @@ -0,0 +1,30 @@ +import {NativeClientBackend} from "tc-shared/backend/NativeClient"; +import {ipcRenderer} from "electron"; +import {Arguments, processArguments} from "../../shared/process-arguments"; + +const call_basic_action = (name: string, ...args: any[]) => ipcRenderer.send('basic-action', name, ...args); +export class NativeClientBackendImpl implements NativeClientBackend { + openChangeLog(): void { + call_basic_action("open-changelog"); + } + + openClientUpdater(): void { + call_basic_action("check-native-update"); + } + + openDeveloperTools(): void { + call_basic_action("open-dev-tools"); + } + + quit(): void { + call_basic_action("quit"); + } + + reloadWindow(): void { + call_basic_action("reload-window") + } + + showDeveloperOptions(): boolean { + return processArguments.has_flag(Arguments.DEV_TOOLS); + } +} \ No newline at end of file diff --git a/modules/renderer/hooks/Backend.ts b/modules/renderer/hooks/Backend.ts new file mode 100644 index 0000000..6b3ce0e --- /dev/null +++ b/modules/renderer/hooks/Backend.ts @@ -0,0 +1,4 @@ +import {setBackend} from "tc-shared/backend"; +import {NativeClientBackendImpl} from "../backend-impl/Backend"; + +setBackend(new NativeClientBackendImpl()); \ No newline at end of file diff --git a/modules/renderer/hooks/MenuBar.ts b/modules/renderer/hooks/MenuBar.ts new file mode 100644 index 0000000..c9d21f8 --- /dev/null +++ b/modules/renderer/hooks/MenuBar.ts @@ -0,0 +1,4 @@ +import {setMenuBarDriver} from "tc-shared/ui/frames/menu-bar"; +import {NativeMenuBarDriver} from "../MenuBar"; + +setMenuBarDriver(new NativeMenuBarDriver()); \ No newline at end of file diff --git a/modules/renderer/index.ts b/modules/renderer/index.ts index 4303ca1..f6e0b7e 100644 --- a/modules/renderer/index.ts +++ b/modules/renderer/index.ts @@ -52,7 +52,7 @@ loader.register_task(loader.Stage.JAVASCRIPT, { priority: 80 }); -loader.register_task(loader.Stage.INITIALIZING, { +loader.register_task(loader.Stage.SETUP, { name: "teaclient initialize persistent storage", function: async () => { const storage = require("./PersistentLocalStorage"); @@ -154,7 +154,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { /* all files which replaces a native driver */ try { await import("./version"); - await import("./MenuBarHandler"); + await import("./MenuBar"); await import("./ContextMenu"); await import("./SingleInstanceHandler"); await import("./IconHelper"); @@ -164,6 +164,8 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { await import("./hooks/ExternalModal"); await import("./hooks/ServerConnection"); await import("./hooks/ChangeLogClient"); + await import("./hooks/Backend"); + await import("./hooks/MenuBar"); await import("./UnloadHandler"); await import("./WindowsTrayHandler"); diff --git a/modules/shared/MenuBarDefinitions.ts b/modules/shared/MenuBarDefinitions.ts new file mode 100644 index 0000000..4801775 --- /dev/null +++ b/modules/shared/MenuBarDefinitions.ts @@ -0,0 +1,10 @@ +import {NativeImage} from "electron"; + +export interface NativeMenuBarEntry { + uniqueId: string, + type: "separator" | "normal", + label?: string, + icon?: string, + disabled?: boolean, + children?: NativeMenuBarEntry[] +} \ No newline at end of file diff --git a/package.json b/package.json index c6eaf7e..323525f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "TeaClient", - "version": "1.4.12", + "version": "1.4.13", "description": "", "main": "main.js", "scripts": {