Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 168 additions & 5 deletions src/browser/controllers/main-window.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,100 @@ const owElectronApp = electronApp as overwolf.OverwolfApp;
*/
export class MainWindowController {
private browserWindow: BrowserWindow = null;
private selectedDisplayId: number | null = null;

private get settingsFilePath(): string {
return path.join(electronApp.getPath('userData'), 'window-settings.json');
}

private loadDisplaySettings(): void {
try {
if (fs.existsSync(this.settingsFilePath)) {
const data = JSON.parse(fs.readFileSync(this.settingsFilePath, 'utf-8'));
this.selectedDisplayId = typeof data.selectedDisplayId === 'number'
? data.selectedDisplayId
: null;
}
} catch {
this.selectedDisplayId = null;
}
}

private saveDisplaySettings(): void {
try {
fs.writeFileSync(
this.settingsFilePath,
JSON.stringify({ selectedDisplayId: this.selectedDisplayId }),
'utf-8',
);
} catch (error) {
console.error('Failed to save display settings:', error);
}
}

private resolveTargetDisplay(): Electron.Display {
const all = screen.getAllDisplays();
const primary = screen.getPrimaryDisplay();
if (this.selectedDisplayId !== null) {
const saved = all.find((d) => d.id === this.selectedDisplayId);
if (saved) return saved;
this.selectedDisplayId = null;
this.saveDisplaySettings();
}
return primary;
}

// Base window size was designed for a 1920×1080 physical screen.
private static readonly REF_PHYSICAL_WIDTH = 1920;
private static readonly REF_PHYSICAL_HEIGHT = 1080;
private static readonly BASE_WIDTH = 1504;
private static readonly BASE_HEIGHT = 972;

private calculateWindowSize(display: Electron.Display): { width: number; height: number } {
const { workArea, scaleFactor } = display;

// Use physical pixel dimensions so the window occupies the same screen
// fraction on every monitor regardless of its DPI scale setting.
const physW = workArea.width * scaleFactor;
const physH = workArea.height * scaleFactor;

const ratio = Math.min(
physW / MainWindowController.REF_PHYSICAL_WIDTH,
physH / MainWindowController.REF_PHYSICAL_HEIGHT,
);

// Scale base physical size, then convert back to logical pixels for this display.
let width = Math.round((MainWindowController.BASE_WIDTH * ratio) / scaleFactor);
let height = Math.round((MainWindowController.BASE_HEIGHT * ratio) / scaleFactor);

// Never overflow the work area (4% breathing room).
width = Math.min(width, Math.floor(workArea.width * 0.96));
height = Math.min(height, Math.floor(workArea.height * 0.96));

return { width: Math.max(width, 800), height: Math.max(height, 600) };
}

private moveWindowToDisplay(display: Electron.Display): void {
if (!this.browserWindow || this.browserWindow.isDestroyed()) return;

// Maximized windows ignore setBounds — unmaximize first.
if (this.browserWindow.isMaximized()) {
this.browserWindow.unmaximize();
}

const { width, height } = this.calculateWindowSize(display);
const { x, y, width: wa, height: wah } = display.workArea;
const cx = Math.round(x + (wa - width) / 2);
const cy = Math.round(y + (wah - height) / 2);

// Step 1: move a single point onto the target display so Windows switches
// the window's per-monitor DPI context to the target monitor.
this.browserWindow.setPosition(Math.round(x + wa / 2), Math.round(y + wah / 2));

// Step 2: now that the DPI context matches the target display, apply the
// correct size and final centered position.
this.browserWindow.setBounds({ x: cx, y: cy, width, height });
}

/**
*
Expand Down Expand Up @@ -73,6 +167,43 @@ export class MainWindowController {
// this.lolListener.onGameExit(info.classId);
}

private onGameScreenDetected(gameDisplay: Electron.Display): void {
if (!this.browserWindow || this.browserWindow.isDestroyed()) return;

const allDisplays = screen.getAllDisplays();
if (allDisplays.length <= 1) return;

// Find which display the app window currently sits on
const { x, y, width, height } = this.browserWindow.getBounds();
const appDisplay = screen.getDisplayNearestPoint({
x: Math.round(x + width / 2),
y: Math.round(y + height / 2),
});

// Already on a different screen — nothing to do
if (appDisplay.id !== gameDisplay.id) return;

// Pick the first available display that isn't the game's
const alternative = allDisplays.find((d) => d.id !== gameDisplay.id);
if (!alternative) return;

this.printLogMessage(
`[display] Game launched on display ${gameDisplay.id} — moving app to display ${alternative.id}`,
);

// Move without touching selectedDisplayId so the user's saved preference
// is preserved and restored when the game exits.
this.moveWindowToDisplay(alternative);
}

private onGameExit(): void {
if (!this.browserWindow || this.browserWindow.isDestroyed()) return;
// Restore to user's preferred display (or primary if none saved)
const target = this.resolveTargetDisplay();
this.moveWindowToDisplay(target);
this.printLogMessage(`[display] Game exited — restored app to display ${target.id}`);
}

private onRecorderStatusChanged(status: RecordingStatus) {
this.browserWindow?.webContents?.send('recording-status-changed', status);
}
Expand Down Expand Up @@ -108,15 +239,16 @@ export class MainWindowController {
*
*/
public createAndShow(showDevTools: boolean) {
const desiredWidth = 1504,
desiredHeight = 972,
primaryDisplay = screen.getPrimaryDisplay(),
screenWidth = primaryDisplay.workArea.width,
shouldFullscreen = screenWidth < desiredWidth;
this.loadDisplaySettings();
const target = this.resolveTargetDisplay();
const { width: desiredWidth, height: desiredHeight } = this.calculateWindowSize(target);
const shouldFullscreen = target.workArea.width < desiredWidth;

this.browserWindow = new BrowserWindow({
width: desiredWidth,
height: desiredHeight,
x: Math.round(target.workArea.x + (target.workArea.width - desiredWidth) / 2),
y: Math.round(target.workArea.y + (target.workArea.height - desiredHeight) / 2),
show: true,
frame: false,
fullscreenable: false,
Expand Down Expand Up @@ -265,6 +397,35 @@ export class MainWindowController {
}
});
//----------------------------------------------------------------------------

//----------------------------------------------------------------------------
ipcMain.handle('get-displays', () => {
const primaryDisplay = screen.getPrimaryDisplay();
const displays = screen.getAllDisplays().map((display, index) => ({
id: display.id,
label: display.label || `Display ${index + 1}`,
width: display.bounds.width,
height: display.bounds.height,
scaleFactor: display.scaleFactor,
isPrimary: display.id === primaryDisplay.id,
x: display.bounds.x,
y: display.bounds.y,
displayFrequency: display.displayFrequency,
}));
return { displays, selectedDisplayId: this.selectedDisplayId };
});
//----------------------------------------------------------------------------

//----------------------------------------------------------------------------
ipcMain.handle('set-window-display', (_, displayId: number) => {
const display = screen.getAllDisplays().find((d) => d.id === displayId);
if (!display) return false;
this.selectedDisplayId = displayId;
this.saveDisplaySettings();
this.moveWindowToDisplay(display);
return true;
});
//----------------------------------------------------------------------------
}

private registerListeners() {
Expand All @@ -274,6 +435,8 @@ export class MainWindowController {

this.overlayController.on('log', this.printLogMessage.bind(this));
this.overlayController.on('game-exit', this.gepOnExit.bind(this));
this.overlayController.on('game-exit', this.onGameExit.bind(this));
this.overlayController.on('game-screen-detected', this.onGameScreenDetected.bind(this));
this.overlayController.on('show-hide-desktop-window', () => {
this.handleShowHideDesktopWindow();
});
Expand Down
17 changes: 17 additions & 0 deletions src/browser/controllers/overlay/overlay.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export class OverlayController extends PackageControllerBase {
private _ingameWindowsController: IngameWindowsController;
private _exclusiveModeWindowController: ExclusiveModeWindowController;
private _initializedControllers: boolean = false;
// Tracks whether we've already emitted the screen for the current game session
private _gameScreenDetected: boolean = false;
/**
* Constructor for the OverlayController.
* @param overlayService - The service responsible for managing overlay windows.
Expand Down Expand Up @@ -109,6 +111,13 @@ export class OverlayController extends PackageControllerBase {
this._overlayApi.on('game-injected', async (gameInfo) => {
this.log('Game Injected', gameInfo);

// Emit the game's display as soon as we have it after injection
const activeInfo = this._overlayApi.getActiveGameInfo();
if (!this._gameScreenDetected && activeInfo?.gameWindowInfo?.screen) {
this._gameScreenDetected = true;
this.emit('game-screen-detected', activeInfo.gameWindowInfo.screen);
}

// Create an in-game window as an example
try {
//don't create in-game window for 109021 (LoL Launcher)
Expand All @@ -127,6 +136,13 @@ export class OverlayController extends PackageControllerBase {
this._overlayApi.on('game-window-changed', (window, game, reason) => {
this.log('Game Window Changed', reason, window, game);

// Fallback: emit game screen from the first window-changed event if
// getActiveGameInfo() didn't have screen info yet at injection time.
if (!this._gameScreenDetected && window.screen) {
this._gameScreenDetected = true;
this.emit('game-screen-detected', window.screen);
}

// Update exclusive mode window size if needed
this._exclusiveModeWindowController.onGameWindowChanged();
});
Expand All @@ -153,6 +169,7 @@ export class OverlayController extends PackageControllerBase {

this._overlayApi.on('game-exit', (info) => {
this.log('Game Exit', info);
this._gameScreenDetected = false;
this.emit('game-exit', info);

// Close all in-game windows
Expand Down
6 changes: 6 additions & 0 deletions src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ contextBridge.exposeInMainWorld('app', {
checkForUpdates: () => {
return ipcRenderer.invoke('check-for-updates');
},
getDisplays: () => {
return ipcRenderer.invoke('get-displays');
},
setWindowDisplay: (displayId: number) => {
return ipcRenderer.invoke('set-window-display', displayId);
},
});

contextBridge.exposeInMainWorld('privacyApi', {
Expand Down
7 changes: 7 additions & 0 deletions src/renderer/app/components/app-settings/app-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SectionHeader from '../layout/section-header';
import HotKeysSettings from '../hotkeys/hotkeys-settings';
import CheckForUpdates from '../check-for-updates/CheckForUpdates';
import AppContext from '../../context/app-context';
import DisplaySettings from './display-settings';

const AppSettings: React.FC = () => {
const { availablePackages } = useContext(AppContext);
Expand Down Expand Up @@ -38,6 +39,12 @@ const AppSettings: React.FC = () => {
content: <HotKeysSettings />,
isDisabled: !availablePackages.isPackageAvailable('overlay'),
},
{
type: 'expanded-type',
title: 'Display Settings',
description: 'Detect connected displays, resolutions, and scaling',
content: <DisplaySettings />,
},
{
type: 'simple-type',
title: 'Check for updates',
Expand Down
85 changes: 85 additions & 0 deletions src/renderer/app/components/app-settings/display-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { FC, useEffect, useState } from 'react';

interface DisplayInfo {
id: number;
label: string;
width: number;
height: number;
scaleFactor: number;
isPrimary: boolean;
x: number;
y: number;
displayFrequency: number;
}

const DisplaySettings: FC = () => {
const [displays, setDisplays] = useState<DisplayInfo[]>([]);
const [selectedId, setSelectedId] = useState<number | null>(null);

useEffect(() => {
window.app.getDisplays().then(
({ displays: all, selectedDisplayId }: { displays: DisplayInfo[]; selectedDisplayId: number | null }) => {
setDisplays(all);
const initialId = selectedDisplayId ?? all.find((d) => d.isPrimary)?.id ?? null;
setSelectedId(initialId);
},
);
}, []);

const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const id = Number(e.target.value);
setSelectedId(id);
window.app.setWindowDisplay(id);
};

const selected = displays.find((d) => d.id === selectedId);

const displayLabel = (d: DisplayInfo, index: number) => {
const name = d.label || (d.isPrimary ? 'Primary Display' : `Display ${index + 1}`);
return `${name} — ${d.width}×${d.height} @ ${Math.round(d.scaleFactor * 100)}%`;
};

return (
<div className="display-settings">
<div className="settings-input-item">
<label htmlFor="display-select">Select display</label>
<select
id="display-select"
value={selectedId ?? ''}
onChange={handleChange}
>
{displays.map((d, i) => (
<option key={d.id} value={d.id}>
{displayLabel(d, i)}
</option>
))}
</select>
</div>

{selected && (
<dl className="display-info-grid">
<dt>Resolution</dt>
<dd>{selected.width} × {selected.height} px</dd>

<dt>Scale</dt>
<dd>{Math.round(selected.scaleFactor * 100)}%</dd>

{selected.displayFrequency > 0 && (
<>
<dt>Refresh rate</dt>
<dd>{selected.displayFrequency} Hz</dd>
</>
)}

<dt>Position</dt>
<dd>{selected.x}, {selected.y}</dd>

<dt>Primary</dt>
<dd>{selected.isPrimary ? 'Yes' : 'No'}</dd>
</dl>
)}
</div>
);
};

export default DisplaySettings;
Loading