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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ node_modules/
# os crap
Thumbs.db
~*
.DS_Store
Binary file added audio/XP_Exclamation.wav
Binary file not shown.
Binary file added fonts/PerfectDOSVGA437Win.woff
Binary file not shown.
Binary file added fonts/PerfectDOSVGA437Win.woff2
Binary file not shown.
Binary file added fonts/Trebuchet-MS-Italic.ttf
Binary file not shown.
Binary file added fonts/ms_sans_serif.woff
Binary file not shown.
Binary file added fonts/ms_sans_serif.woff2
Binary file not shown.
Binary file added fonts/ms_sans_serif_bold.woff
Binary file not shown.
Binary file added fonts/ms_sans_serif_bold.woff2
Binary file not shown.
Binary file added fonts/trebuc.ttf
Binary file not shown.
1 change: 1 addition & 0 deletions help/nobgcolor.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@import "../lib/os-gui/build/windows-98.css";

body {
background: transparent;
font-size: 70%;
font-family: Verdana, Arial, Helvetica, "MS Sans Serif";
}
Expand Down
Binary file added images/error-xp-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/icons/xp-paint-128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/info-xp-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/question-xp-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/warning-xp-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions lib/os-gui/$Window.js
Original file line number Diff line number Diff line change
Expand Up @@ -1314,8 +1314,8 @@ You can also disable this warning by passing {iframes: {ignoreCrossOrigin: true}

$w.center = () => {
$w.css({
left: (innerWidth - $w.width()) / 2 + window.scrollX,
top: (innerHeight - $w.height()) / 2 + window.scrollY,
left: Math.round((innerWidth - $w.width()) / 2 + window.scrollX),
top: Math.round((innerHeight - $w.height()) / 2 + window.scrollY),
});
$w.applyBounds();
};
Expand Down Expand Up @@ -1347,8 +1347,8 @@ You can also disable this warning by passing {iframes: {ignoreCrossOrigin: true}
drag_pointer_y = e.clientY ?? drag_pointer_y;
}
$w.css({
left: drag_pointer_x + scrollX - drag_offset_x,
top: drag_pointer_y + scrollY - drag_offset_y,
left: Math.round(drag_pointer_x + scrollX - drag_offset_x),
top: Math.round(drag_pointer_y + scrollY - drag_offset_y),
});
};
$w.$titlebar.css("touch-action", "none");
Expand Down
18 changes: 18 additions & 0 deletions lib/os-gui/parse-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,24 @@ function applyCSSProperties(cssProperties, options = {}) {
}
}

window.applyTheme = applyCSSProperties;

/**
* @param {HTMLElement} element
* @returns {Record<string, string>}
*/
window.getThemeCSSProperties = function (element) {
const styles = getComputedStyle(element);
const properties = {};
for (let i = 0; i < styles.length; i++) {
const prop = styles[i];
if (prop.startsWith("--")) {
properties[prop] = styles.getPropertyValue(prop);
}
}
return properties;
};

/**
* @param {Record<string, string>} cssProperties
* @returns {string}
Expand Down
56 changes: 53 additions & 3 deletions src/electron-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,25 +160,75 @@ const createWindow = () => {

// Emitted when the window is closed.
editor_window.on("closed", () => {
// If we were quitting the app, actually quit now (for Mac)
if (process.platform === "darwin" && is_quitting_app_mac) {
is_quitting_app_mac = false;
app.quit();
}
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
editor_window = null;
});

// Emitted before the window is closed.
// Track if we're in the process of quitting the app (for Mac)
let is_quitting_app_mac = false;

// Handle app-level quit (from menu Quit or Cmd+Q, only on Mac)
app.on("before-quit", (event) => {
if (process.platform === "darwin") {
// Mark that we're quitting so that when the window's "close" event is triggered,
// We know to quit the entire app after the save prompt (vs just closing the window).
if (editor_window && !editor_window.isDestroyed()) {
is_quitting_app_mac = true;
}
}
});

// Listen for response from renderer about whether we can quit
ipcMain.on("can-quit", () => {
if (process.platform === "darwin") {
if (is_quitting_app_mac) {
is_quitting_app_mac = false;
// Close the window first (if it exists and isn't destroyed)
if (editor_window && !editor_window.isDestroyed()) {
editor_window.close();
}
// On macOS, closing the window doesn't quit the app, so quit explicitly
// The closed handler will also call app.quit() as a backup
app.quit();
}
}
});

// Listen for cancel quit
ipcMain.on("cancel-quit", () => {
if (process.platform === "darwin") {
is_quitting_app_mac = false;
}
});

// Emitted before the window is closed (Mac: For window closing, not app quitting)
editor_window.on("close", (event) => {
// This is just closing the window (e.g., clicking X button) on Mac. On any other platform, ignore this comment
// Don't need to check editor_window.isDocumentEdited(),
// because the (un)edited state is handled by the renderer process, in are_you_sure().
// Note: if the web contents are not responding, this will make the app harder to close.
// Similarly, if there's an error, the app will be harder to close (perhaps worse as it's less likely to show a Not Responding dialog).
// And this also prevents it from closing with Ctrl+C in the terminal, which is arguably a feature.
// TODO: focus window if it's not focused, which can happen via right clicking the dock/taskbar icon, or Ctrl+C in the terminal
// (but ideally not if it's going to close without prompting)
editor_window.webContents.send("close-window-prompt");
if (process.platform === "darwin") {
if (editor_window && !editor_window.isDestroyed()) {
activate_app(); // Focus/show window
editor_window.webContents.send("close-window-prompt");
}
} else {
editor_window.webContents.send("close-window-prompt");
}

event.preventDefault();
});

// Open links without target=_blank externally.
editor_window.webContents.on("will-navigate", (e, url) => {
// check that the URL is not part of the app
Expand Down
32 changes: 28 additions & 4 deletions src/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { apply_image_transformation, draw_grid, draw_selection_box, flip_horizon
import { show_imgur_uploader } from "./imgur.js";
import { showMessageBox } from "./msgbox.js";
import { localStore } from "./storage.js";
import { get_theme } from "./theme.js";
import { TOOL_CURVE, TOOL_FREE_FORM_SELECT, TOOL_POLYGON, TOOL_SELECT, TOOL_TEXT, tools } from "./tools.js";
// `sessions.js` must be loaded after `app.js`
// This would cause it to be loaded earlier, and error trying to access `undos`
Expand Down Expand Up @@ -747,6 +748,10 @@ function update_title() {

if (is_pride_month) {
$("link[rel~='icon']").attr("href", "./images/icons/gay-es-paint-16x16-light-outline.png");
} else if (get_theme() === "windows-xp.css") {
$("#about-paint-icon").attr("src", "./images/icons/xp-paint-128x128.png");
} else {
$("#about-paint-icon").attr("src", "images/icons/128x128.png");
}

if (window.setRepresentedFilename) {
Expand Down Expand Up @@ -1539,6 +1544,21 @@ function show_file_format_errors({ as_image_error, as_palette_error }) {
let $about_paint_window;
const $about_paint_content = $("#about-paint");

function update_about_paint_icon() {
const $icon = $("#about-paint-icon");
if (!$icon.length) {
return;
}
if (is_pride_month) {
$icon.attr("src", "./images/icons/gay-es-paint-128x128.png");
} else if (get_theme() === "windows-xp.css") {
$icon.attr("src", "./images/icons/xp-paint-128x128.png");
} else {
$icon.attr("src", "images/icons/128x128.png");
}
}
$G.on("theme-load", update_about_paint_icon);

/** @type {OSGUI$Window} */
let $news_window;
const $this_version_news = $("#news");
Expand All @@ -1565,9 +1585,8 @@ function show_about_paint() {
minimizeButton: false,
});
$about_paint_window.addClass("about-paint squish");
if (is_pride_month) {
$("#about-paint-icon").attr("src", "./images/icons/gay-es-paint-128x128.png");
}

update_about_paint_icon();

$about_paint_window.$content.append($about_paint_content.show()).css({ padding: "15px" });

Expand Down Expand Up @@ -1936,7 +1955,7 @@ function render_history_as_gif() {
$win.title("Rendered GIF");
const blob_url = URL.createObjectURL(blob);
$output.empty().append(
$(E("div")).addClass("inset-deep").append(
$(E("div")).addClass("inset-deep rendered-gif-preview").append(
$(E("img")).attr({
src: blob_url,
width,
Expand Down Expand Up @@ -2295,6 +2314,11 @@ function show_document_history() {
$mode_select.css({
margin: "10px",
});

// Hotfix for bug where the dropdown would close immediately when clicked in Chrome.
$mode_select[0].focus = () => { };
$w.$content[0].focus = () => { };

let mode = $mode_select.val();
$mode_select.on("change", () => {
mode = $mode_select.val();
Expand Down
4 changes: 3 additions & 1 deletion src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,10 @@ interface Window {
// help.js
jQuery: JQueryStatic; // help.js reaches into iframe to use jQuery
MouseEvent: typeof MouseEvent; // help.js reaches into iframe and uses MouseEvent
applyTheme: (cssProperties: Record<string, string>, documentElement?: HTMLElement) => void; // this is defined in 98.js.org... does it actually exist [when running in 98.js.org]? btw HTMLHtmlElement would be more specific, but document.documentElement is typed as HTMLElement, so it'd just be annoying
applyTheme: (cssProperties: Record<string, string>, options?: { element?: HTMLElement, recurseIntoIframes?: boolean }) => void; // this is defined in 98.js.org... does it actually exist [when running in 98.js.org]? btw HTMLHtmlElement would be more specific, but document.documentElement is typed as HTMLElement, so it'd just be annoying
themeCSSProperties: Record<string, string>;
theme_file_name: string;
getThemeCSSProperties: (element: HTMLElement) => Record<string, string>;
// file-format-data.js
image_formats: ImageFileFormat[];
palette_formats: PaletteFileFormat[];
Expand Down
10 changes: 9 additions & 1 deletion src/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,15 @@ function $Iframe(options) {
// This try-catch may only be needed for running Cypress tests.
try {
if (window.themeCSSProperties && window.applyTheme) {
window.applyTheme(window.themeCSSProperties, iframe.contentDocument.documentElement);
window.applyTheme(window.themeCSSProperties, { element: iframe.contentDocument.documentElement, recurseIntoIframes: true });
}
if (window.theme_file_name) {
const theme_link = iframe.contentDocument.createElement("link");
theme_link.rel = "stylesheet";
theme_link.type = "text/css";
theme_link.href = `../styles/themes/${window.theme_file_name}`;
theme_link.id = "theme-link";
iframe.contentDocument.head.appendChild(theme_link);
}

// on Wayback Machine, and iframe's url not saved yet
Expand Down
14 changes: 14 additions & 0 deletions src/menus.js
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,20 @@ const menus = {
enabled: () => get_theme() != "bubblegum.css",
description: localize("Makes JS Paint look like pearlescent bubblegum."),
},
{
emoji_icon: "🟦",
label: localize("Windows &XP"),
speech_recognition: [
"windows xp theme", "switch to windows xp theme", "use windows xp theme", "set theme to windows xp", "set theme windows xp", "switch to windows xp theme", "switch theme to windows xp", "switch theme windows xp",
"xp theme", "switch to xp theme", "use xp theme", "set theme to xp", "set theme xp", "switch to xp theme", "switch theme to xp", "switch theme xp",
"windows xp", "xp",
],
action: () => {
set_theme("windows-xp.css");
},
enabled: () => get_theme() != "windows-xp.css",
description: localize("Makes JS Paint look like Windows XP."),
},
// {
// emoji_icon: "🪐",
// label: localize("&Retro Futurist"),
Expand Down
28 changes: 24 additions & 4 deletions src/msgbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,37 @@
// or, couldn't we just provide the default in a wrapper function, similar to how 98.js.org does it?

import { make_window_supporting_scale } from "./$ToolWindow.js";
import { get_theme } from "./theme.js";
// import { localize } from "./app-localization.js";

const exports = {};

const CHORD_WAV_URL = "audio/chord.wav";
const XP_EXCLAMATION_WAV_URL = "audio/XP_Exclamation.wav";

try {
// <audio> element is simpler for sound effects,
// but in iOS/iPad it shows up in the Control Center, as if it's music you'd want to play/pause/etc.
// It's very silly. Also, on subsequent plays, it only plays part of the sound.
// And Web Audio API is better for playing SFX anyway because it can play a sound overlapping with itself.
const audioContext = window.audioContext = window.audioContext || new AudioContext();
const audio_buffer_promise =
const chord_buffer_promise =
fetch(CHORD_WAV_URL)
.then((response) => response.arrayBuffer())
.then((array_buffer) => audioContext.decodeAudioData(array_buffer));
const xp_exclamation_buffer_promise =
fetch(XP_EXCLAMATION_WAV_URL)
.then((response) => response.arrayBuffer())
.then((array_buffer) => audioContext.decodeAudioData(array_buffer));
var play_chord = async function () {
audioContext.resume(); // in case it was not allowed to start until a user interaction
// Note that this should be before waiting for the audio buffer,
// so that it works the first time.
// (This only works if the message box is opened during a user gesture.)

const audio_buffer = await audio_buffer_promise;
// Use XP exclamation sound for Windows XP theme, default chord for all others
const is_xp_theme = get_theme() === "windows-xp.css";
const audio_buffer = await (is_xp_theme ? xp_exclamation_buffer_promise : chord_buffer_promise);
const source = audioContext.createBufferSource();
source.buffer = audio_buffer;
source.connect(audioContext.destination);
Expand All @@ -47,7 +55,7 @@ try {
* @property {string} [message]
* @property {string} [messageHTML]
* @property {Array<{ label: string, value: string, default?: boolean, action?: () => void }>} [buttons]
* @property {"error" | "warning" | "info" | "nuke"} [iconID]
* @property {"error" | "warning" | "info" | "question" | "nuke"} [iconID]
* @property {OSGUIWindowOptions} [windowOptions]
*
* @typedef {Promise<string> & { $window: JQuery<Window>, $message: JQuery<HTMLDivElement>, promise: MessageBoxPromise }} MessageBoxPromise
Expand Down Expand Up @@ -91,8 +99,20 @@ function showMessageBox_implementation({
wordWrap: "break-word",
});
}
let icon_src = `images/${iconID}-32x32-8bpp.png`;
if (get_theme() === "windows-xp.css") {
if (iconID === "warning") {
icon_src = "images/warning-xp-32x32.png";
} else if (iconID === "error") {
icon_src = "images/error-xp-32x32.png";
} else if (iconID === "info") {
icon_src = "images/info-xp-32x32.png";
} else if (iconID === "question") {
icon_src = "images/question-xp-32x32.png";
}
}
$("<div>").append(
$("<img width='32' height='32'>").attr("src", `images/${iconID}-32x32-8bpp.png`).css({
$("<img width='32' height='32'>").attr("src", icon_src).css({
margin: "16px",
display: "block",
}),
Expand Down
30 changes: 24 additions & 6 deletions src/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,29 @@ theme_link.href = href_for(current_theme);
theme_link.id = "theme-link";
document.head.appendChild(theme_link);

/** @type {any} */
const window_any = window;

window_any.theme_file_name = current_theme;
function update_theme_css_properties() {
if (window_any.getThemeCSSProperties) {
window_any.themeCSSProperties = window_any.getThemeCSSProperties(document.documentElement);
}
}
// Initial properties may not be ready if getThemeCSSProperties is defined later (lib/os-gui/parse-theme.js)
setTimeout(update_theme_css_properties, 0);

update_not_for_modern_theme();

const get_theme = () => current_theme;

const signal_theme_load = () => {
if (window_any.$) {
window_any.$(window).triggerHandler("theme-load");
window_any.$(window).trigger("resize"); // not exactly, but get dynamic cursor to update its offset
}
};

const set_theme = (theme) => {
current_theme = theme;

Expand All @@ -58,13 +77,12 @@ const set_theme = (theme) => {
grinch_button?.remove();
} catch (_error) { /* ignore */ }

const signal_theme_load = () => {
$(window).triggerHandler("theme-load");
$(window).trigger("resize"); // not exactly, but get dynamic cursor to update its offset
};

wait_for_theme_loaded(theme, signal_theme_load);
wait_for_theme_loaded(theme, () => {
signal_theme_load();
update_theme_css_properties();
});
theme_link.href = href_for(theme);
window_any.theme_file_name = theme;

update_not_for_modern_theme();

Expand Down
7 changes: 6 additions & 1 deletion styles/themes/classic.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@
.jspaint {
background: var(--ButtonFace);
}
body,
body {
background: transparent;
}
.help-window iframe {
background: #fff;
}
.canvas-area {
background: var(--AppWorkspace);
}
Expand Down
Loading