Committing updates and pushed version
This commit is contained in:
		
							parent
							
								
									7087514df1
								
							
						
					
					
						commit
						a636515de8
					
				
							
								
								
									
										2
									
								
								github
									
									
									
									
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								github
									
									
									
									
									
								
							| @ -1 +1 @@ | ||||
| Subproject commit 9c3cc6d05838a03a5827836b300f8bc8e71b26d2 | ||||
| Subproject commit 7c087d46ad75ff641d5862a57ff13f3e860cc8a4 | ||||
| @ -122,5 +122,5 @@ function deploy_client() { | ||||
| #install_npm | ||||
| #compile_scripts | ||||
| #compile_native | ||||
| package_client | ||||
| #deploy_client | ||||
| #package_client | ||||
| deploy_client | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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<ExternalModal> implements ExternalModal { | ||||
| @ -71,15 +71,16 @@ class ProxyImplementation extends ProxiedClass<ExternalModal> 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); | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| }); | ||||
| @ -5,7 +5,7 @@ import {loadWindowBounds, startTrackWindowBounds} from "../../shared/window"; | ||||
| let global_window: electron.BrowserWindow; | ||||
| let global_window_promise: Promise<void>; | ||||
| 
 | ||||
| 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({ | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| }); | ||||
| @ -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()); | ||||
|     } | ||||
| }) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										99
									
								
								modules/renderer/MenuBar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								modules/renderer/MenuBar.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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> | 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); | ||||
| @ -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); | ||||
|  | ||||
							
								
								
									
										30
									
								
								modules/renderer/backend-impl/Backend.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								modules/renderer/backend-impl/Backend.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										4
									
								
								modules/renderer/hooks/Backend.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/renderer/hooks/Backend.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| import {setBackend} from "tc-shared/backend"; | ||||
| import {NativeClientBackendImpl} from "../backend-impl/Backend"; | ||||
| 
 | ||||
| setBackend(new NativeClientBackendImpl()); | ||||
							
								
								
									
										4
									
								
								modules/renderer/hooks/MenuBar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								modules/renderer/hooks/MenuBar.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| import {setMenuBarDriver} from "tc-shared/ui/frames/menu-bar"; | ||||
| import {NativeMenuBarDriver} from "../MenuBar"; | ||||
| 
 | ||||
| setMenuBarDriver(new NativeMenuBarDriver()); | ||||
| @ -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"); | ||||
|  | ||||
							
								
								
									
										10
									
								
								modules/shared/MenuBarDefinitions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								modules/shared/MenuBarDefinitions.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import {NativeImage} from "electron"; | ||||
| 
 | ||||
| export interface NativeMenuBarEntry { | ||||
|     uniqueId: string, | ||||
|     type: "separator" | "normal", | ||||
|     label?: string, | ||||
|     icon?: string, | ||||
|     disabled?: boolean, | ||||
|     children?: NativeMenuBarEntry[] | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "TeaClient", | ||||
|   "version": "1.4.12", | ||||
|   "version": "1.4.13", | ||||
|   "description": "", | ||||
|   "main": "main.js", | ||||
|   "scripts": { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user