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…
Reference in New Issue
Block a user