Update os-gui to 0.6.0

main
Isaiah Odhner 2021-11-20 00:03:36 -05:00
parent 91a6466a5d
commit d7e66471de
5 changed files with 247 additions and 84 deletions

View File

@ -84,10 +84,16 @@ $Window.Z_INDEX = 5;
var minimize_slots = []; // for if there's no taskbar
// @TODO: make this a class,
// instead of a weird pseudo-class
function $Window(options) {
options = options || {};
// @TODO: handle all option defaults here
// and validate options.
var $w = $(E("div")).addClass("window os-window").appendTo("body");
$w[0].$window = $w;
$w.element = $w[0];
$w[0].id = `os-window-${Math.random().toString(36).substr(2, 9)}`;
$w.$titlebar = $(E("div")).addClass("window-titlebar").appendTo($w);
$w.$title_area = $(E("div")).addClass("window-title-area").appendTo($w.$titlebar);
@ -99,14 +105,20 @@ function $Window(options) {
if (options.minimizeButton !== false) {
$w.$minimize = $(E("button")).addClass("window-minimize-button window-action-minimize window-button").appendTo($w.$titlebar);
$w.$minimize.attr("aria-label", "Minimize window"); // @TODO: for taskbarless minimized windows, "restore"
$w.$minimize.append("<span class='window-button-icon'></span>");
}
if (options.maximizeButton !== false) {
$w.$maximize = $(E("button")).addClass("window-maximize-button window-action-maximize window-button").appendTo($w.$titlebar);
$w.$maximize.attr("aria-label", "Maximize or restore window"); // @TODO: specific text for the state
if (!options.resizable) {
$w.$maximize.attr("disabled", true);
}
$w.$maximize.append("<span class='window-button-icon'></span>");
}
if (options.closeButton !== false) {
$w.$x = $(E("button")).addClass("window-close-button window-action-close window-button").appendTo($w.$titlebar);
$w.$x.attr("aria-label", "Close window");
$w.$x.append("<span class='window-button-icon'></span>");
}
$w.$content = $(E("div")).addClass("window-content").appendTo($w);
$w.$content.attr("tabIndex", "-1");
@ -146,6 +158,7 @@ function $Window(options) {
$w.$icon.prependTo($w.$titlebar);
}
iconSize = target_icon_size;
$w.trigger("icon-change");
};
$w.getTitlebarIconSize = function () {
return iconSize;
@ -203,12 +216,14 @@ function $Window(options) {
old_$icon.replaceWith($w.$icon);
$w.icon_name = icon_name;
$w.task?.updateIcon();
$w.trigger("icon-change");
return $w;
};
$w.setIcons = (icons) => {
$w.icons = icons;
$w.setTitlebarIconSize(iconSize);
$w.task?.updateIcon();
// icon-change already sent by setTitlebarIconSize
};
if ($component) {
@ -552,19 +567,33 @@ function $Window(options) {
$w.css("touch-action", "none");
let minimize_target_el = null; // taskbar button (optional)
$w.setMinimizeTarget = function (new_taskbar_button_el) {
minimize_target_el = new_taskbar_button_el;
};
let task;
Object.defineProperty($w, "task", {
get() {
return task;
},
set(new_task) {
console.warn("DEPRECATED: use $w.setMinimizeTarget(taskbar_button_el) instead of setting $window.task object");
task = new_task;
},
});
let before_minimize;
$w.minimize = () => {
minimize_target_el = minimize_target_el || task?.$task[0];
if (animating_titlebar) {
when_done_animating_titlebar.push($w.minimize);
return;
}
if ($w.is(":visible")) {
if ($w.task) {
// @TODO: API like $w.setMinimizeTarget(taskbarItemElement)
// instead of this hacky way you have to set `task` on the window
const $task = $w.task.$task;
if (minimize_target_el && !$w.hasClass("minimized-without-taskbar")) {
const before_rect = $w.$titlebar[0].getBoundingClientRect();
const after_rect = $task[0].getBoundingClientRect();
const after_rect = minimize_target_el.getBoundingClientRect();
$w.animateTitlebar(before_rect, after_rect, () => {
$w.hide();
$w.blur();
@ -693,8 +722,7 @@ function $Window(options) {
return;
}
if ($w.is(":hidden")) {
const $task = $w.task.$task;
const before_rect = $task[0].getBoundingClientRect();
const before_rect = minimize_target_el.getBoundingClientRect();
$w.show();
const after_rect = $w.$titlebar[0].getBoundingClientRect();
$w.hide();
@ -708,6 +736,9 @@ function $Window(options) {
let before_maximize;
$w.maximize = () => {
if (!options.resizable) {
return;
}
if (animating_titlebar) {
when_done_animating_titlebar.push($w.maximize);
return;
@ -1150,7 +1181,7 @@ You can also disable this warning by passing {iframes: {ignoreCrossOrigin: true}
}
break;
}
case 27: // Esc
case 27: // Escape
// @TODO: make this optional, and probably default false
$w.close();
break;
@ -1221,6 +1252,9 @@ You can also disable this warning by passing {iframes: {ignoreCrossOrigin: true}
refocus();
// Emulate :enabled:active:hover state with .pressing class
const button = e.currentTarget;
if (!$(button).is(":enabled")) {
return;
}
button.classList.add("pressing");
const release = (event) => {
// blur is just to handle the edge case of alt+tabbing/ctrl+tabbing away
@ -1445,6 +1479,7 @@ You can also disable this warning by passing {iframes: {ignoreCrossOrigin: true}
$w.title = title => {
if (title) {
$w.$title.text(title);
$w.trigger("title-change");
if ($w.task) {
$w.task.updateTitle();
}
@ -1536,6 +1571,22 @@ You can also disable this warning by passing {iframes: {ignoreCrossOrigin: true}
};
$w.closed = false;
let current_menu_bar;
// @TODO: should this be like setMenus(menu_definitions)?
// It seems like setMenuBar(menu_bar) might be prone to bugs
// trying to set the same menu bar on multiple windows.
$w.setMenuBar = (menu_bar) => {
// $w.find(".menus").remove(); // ugly, if only because of the class name haha
if (current_menu_bar) {
current_menu_bar.element.remove();
}
if (menu_bar) {
$w.$titlebar.after(menu_bar.element);
menu_bar.setKeyboardScope($w[0]);
current_menu_bar = menu_bar;
}
};
if (options.title) {
$w.title(options.title);
}

View File

@ -162,7 +162,7 @@ function MenuBar(menus) {
}
top_level_highlight(-1);
});
const is_disabled = item => {
if (typeof item.enabled === "function") {
@ -334,20 +334,34 @@ function MenuBar(menus) {
default:
// handle accelerators and first-letter navigation
const key = String.fromCharCode(e.keyCode).toLowerCase();
const item_els = [...menu_popup_el.querySelectorAll(".menu-item")];
const item_els = active_menu_popup ?
[...menu_popup_el.querySelectorAll(".menu-item")] :
top_level_menus.map(top_level_menu => top_level_menu.menu_button_el);
const item_els_by_accelerator = {};
for (const item_el of item_els) {
const accelerator = item_el.querySelector(".menu-hotkey");
const accelerator_key = (accelerator ? accelerator.textContent : item_el.querySelector(".menu-item-label").textContent[0]).toLowerCase();
const accelerator_key = (accelerator ?
accelerator.textContent :
(item_el.querySelector(".menu-item-label") ?? item_el).textContent[0]
).toLowerCase();
item_els_by_accelerator[accelerator_key] = item_els_by_accelerator[accelerator_key] || [];
item_els_by_accelerator[accelerator_key].push(item_el);
}
const matching_item_els = item_els_by_accelerator[key] || [];
// console.log({ key, item_els, item_els_by_accelerator, matching_item_els });
if (matching_item_els.length) {
if (matching_item_els.length === 1) {
// it's unambiguous, go ahead and activate it
const menu_item_el = matching_item_els[0];
menu_item_el.click();
// click() doesn't work for menu buttons at the moment,
// and also we want to highlight the first item in the menu
// in that case, which doesn't happen with the mouse
if (menu_item_el.classList.contains("menu-button")) {
const top_level_menu = top_level_menus.find(top_level_menu => top_level_menu.menu_button_el === menu_item_el);
top_level_menu.open_top_level_menu("keydown");
} else {
menu_item_el.click();
}
e.preventDefault();
} else {
// cycle the menu items that match the key
@ -814,28 +828,14 @@ function MenuBar(menus) {
menu_button_el.setAttribute("aria-haspopup", "true");
menu_button_el.setAttribute("aria-controls", menu_popup_el.id);
// @TODO: allow setting scope for alt shortcuts, like menuBar.setHotkeyScope(windowElement||window)
// and add a helper to $Window to set up a menu bar, like $window.setMenuBar(menuBar||null)
window.addEventListener("keydown", e => {
if (e.ctrlKey || e.metaKey) { // Ctrl or Command held
if (e.keyCode !== 17 && e.keyCode !== 91 && e.keyCode !== 93 && e.keyCode !== 224) { // anything but Ctrl or Command pressed
close_menus();
}
return;
}
if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) { // Alt held
if (String.fromCharCode(e.keyCode) === get_hotkey(menus_key)) {
e.preventDefault();
open_top_level_menu("keydown");
}
}
});
menu_button_el.addEventListener("focus", () => {
top_level_highlight(menus_key);
});
menu_button_el.addEventListener("pointerdown", e => {
if (menu_button_el.classList.contains("active")) {
menu_button_el.dispatchEvent(new CustomEvent("release", {}));
refocus_window();
e.preventDefault(); // needed for refocus_window() to work
} else {
open_top_level_menu(e.type);
}
@ -863,7 +863,7 @@ function MenuBar(menus) {
// console.log("pointerdown (possibly simulated) — menu_popup_el.style.zIndex", menu_popup_el.style.zIndex, "$Window.Z_INDEX", $Window.Z_INDEX, "menus_el.closest('.window').style.zIndex", menus_el.closest(".window").style.zIndex);
// setTimeout(() => { console.log("after timeout, menus_el.closest('.window').style.zIndex", menus_el.closest(".window").style.zIndex); }, 0);
top_level_highlight(menus_key);
menu_popup_el.dispatchEvent(new CustomEvent("update"), {});
selecting_menus = true;
@ -894,6 +894,7 @@ function MenuBar(menus) {
menu_button_el,
menu_popup_el,
menus_key,
hotkey: get_hotkey(menus_key),
open_top_level_menu,
});
};
@ -918,7 +919,15 @@ function MenuBar(menus) {
}
}
});
window.addEventListener("blur", close_menus);
// window.addEventListener("blur", close_menus);
window.addEventListener("blur", (event) => {
// hack for Pinball (in 98.js.org) where it triggers fake blur events
// in order to pause the game
if (!event.isTrusted) {
return;
}
close_menus();
});
function close_menus_on_click_outside(event) {
if (event.target?.closest?.(".menus, .menu-popup")) {
return;
@ -929,7 +938,51 @@ function MenuBar(menus) {
window.addEventListener("pointerdown", close_menus_on_click_outside);
window.addEventListener("pointerup", close_menus_on_click_outside);
let keyboard_scope_elements = [];
function set_keyboard_scope(...elements) {
for (const el of keyboard_scope_elements) {
el.removeEventListener("keydown", keyboard_scope_keydown);
}
keyboard_scope_elements = elements;
for (const el of keyboard_scope_elements) {
el.addEventListener("keydown", keyboard_scope_keydown);
}
}
function keyboard_scope_keydown(e) {
// Close menus if the user presses almost any key combination
// e.g. if you look in the menu to remember a shortcut,
// and then use the shortcut.
if (
(e.ctrlKey || e.metaKey) && // Ctrl or Command held down
// and anything then pressed other than Ctrl or Command
e.keyCode !== 17 &&
e.keyCode !== 91 &&
e.keyCode !== 93 &&
e.keyCode !== 224
) {
close_menus();
return;
}
if (e.defaultPrevented) {
return; // closing menus above is meant to be done when activating unrelated shortcuts
// but stuff after this is should not be handled at the same time as something else
}
if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) { // Alt held
const menu = top_level_menus.find((menu) =>
menu.hotkey.toLowerCase() === String.fromCharCode(e.keyCode).toLowerCase()
);
if (menu) {
e.preventDefault();
menu.open_top_level_menu("keydown");
}
}
}
set_keyboard_scope(window);
this.element = menus_el;
this.closeMenus = close_menus;
this.setKeyboardScope = set_keyboard_scope;
}
exports.MenuBar = MenuBar;

View File

@ -276,3 +276,42 @@ function makeThemeCSSFile(cssProperties) {
`;
return css;
}
// @TODO: should this be part of theme graphics generation?
// I want to figure out a better way to do dynamic theme features,
// where it works with the CSS cascade as much as possible
function makeBlackToInsetFilter() {
if (document.getElementById("os-gui-black-to-inset-filter")) {
return;
}
const svg_xml = `
<svg style="position: absolute; pointer-events: none; bottom: 100%;">
<defs>
<filter id="os-gui-black-to-inset-filter" x="0" y="0" width="1px" height="1px">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
-1000 -1000 -1000 1 0
"
result="black-parts-isolated"
/>
<feFlood result="shadow-color" flood-color="var(--ButtonShadow)"/>
<feFlood result="hilight-color" flood-color="var(--ButtonHilight)"/>
<feOffset in="black-parts-isolated" dx="1" dy="1" result="offset"/>
<feComposite in="hilight-color" in2="offset" operator="in" result="hilight-colored-offset"/>
<feComposite in="shadow-color" in2="black-parts-isolated" operator="in" result="shadow-colored"/>
<feMerge>
<feMergeNode in="hilight-colored-offset"/>
<feMergeNode in="shadow-colored"/>
</feMerge>
</filter>
</defs>
</svg>
`;
const $svg = $(svg_xml);
$svg.appendTo("body");
}

View File

@ -145,6 +145,7 @@ button:not(.lightweight).default:enabled:hover:active {
}
*/
/* also, this is more complicated; see Paint; the tool buttons translate when being pushed and when depressed, and these add together */
/* omg, a thought... what if I used feDisplacementMap SVG filter... */
button:not(.lightweight):focus::before {
content: "";
@ -316,9 +317,77 @@ body > .window-titlebar {
border-width: 2px 2px;
padding: 2px;
}
.window-button {
display: block;
width: 16px;
height: 14px;
padding: 0;
margin: 2px 0;
}
.window-button-icon {
display: block;
/* background-image: url("images/titlebar-buttons.png"); */
--sprite-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAAKCAYAAADo3z3CAAAAoUlEQVRIS9VVWw7AIAib9z/0FpZgCOFRgpluf9MClhYdV++7gfDhYLxYDw+UyiHd5F8S5lr6zNa6Xpv/KwhHOahQpLB1+CwfycgYrwmE0WK8MTsIR1aOGsR+NYkkYzN5/pGwVA9xA/diq8LeHCKuQxQ+aoYt2yJWtpSNZth0edRpGVC5eGQcSg4hXLml3fdpBeHs8evWyPKX9ruXVqnYCeAHA8IyC9K2kmkAAAAASUVORK5CYII=");
--sprite-y: 0;
-ms-interpolation-mode: nearest-neighbor;
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
width: 12px;
height: 10px;
position: relative;
pointer-events: none;
}
.os-window .window-button:enabled:hover:active .window-button-icon,
.os-window .window-button.pressing .window-button-icon {
top: 1px;
left: 1px;
}
.window-button:disabled .window-button-icon {
/* filter: saturate(0%) opacity(50%); fallback */
/* filter: url("#os-gui-black-to-inset-filter"); */
}
.window-button .window-button-icon::before,
.window-button .window-button-icon::after {
content: "";
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
-webkit-mask-image: var(--sprite-image);
mask-image: var(--sprite-image);
-webkit-mask-position: var(--sprite-x) var(--sprite-y);
mask-position: var(--sprite-x) var(--sprite-y);
background-color: rgb(0, 0, 0);
background-color: var(--ButtonText);
}
.window-button:disabled .window-button-icon::before {
background-color: rgb(255, 255, 255);
background-color: var(--ButtonHilight);
left: 1px;
top: 1px;
}
.window-button:enabled .window-button-icon::after {
display: none;
}
.window-button:disabled .window-button-icon::after {
background-color: rgb(128, 128, 128);
background-color: var(--GrayText);
}
.window-action-close .window-button-icon {
--sprite-x: calc(-3 * 13px - 1px);
}
.window-action-maximize .window-button-icon {
--sprite-x: calc(-1 * 13px - 1px);
}
.window-action-restore .window-button-icon {
--sprite-x: calc(-2 * 13px - 1px);
}
.window-action-minimize .window-button-icon {
--sprite-x: calc(-0 * 13px - 1px);
}
.window-close-button {
margin-left: 2px;
margin-right: 2px;
@ -326,11 +395,11 @@ body > .window-titlebar {
.os-window.tool-window .window-close-button {
width: 11px;
height: 11px;
background-position: 7px 0;
}
.os-window.tool-window .window-close-button:enabled:hover:active,
.os-window.tool-window .window-close-button.pressing {
background-position: 8px 1px;
.os-window.tool-window .window-close-button .window-button-icon {
width: 7px;
height: 7px;
--sprite-x: 7px;
}
.os-window .window-title-area {
height: 16px;
@ -358,55 +427,6 @@ body > .window-titlebar {
white-space: nowrap;
padding-left: 2px;
}
.window-close-button {
/* display: block !important; */
/* float: right; */
/* width: 13px;
height: 11px; */
}
.window-close-button,
.window-maximize-button,
.window-minimize-button {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAAKCAYAAADo3z3CAAAAoUlEQVRIS9VVWw7AIAib9z/0FpZgCOFRgpluf9MClhYdV++7gfDhYLxYDw+UyiHd5F8S5lr6zNa6Xpv/KwhHOahQpLB1+CwfycgYrwmE0WK8MTsIR1aOGsR+NYkkYzN5/pGwVA9xA/diq8LeHCKuQxQ+aoYt2yJWtpSNZth0edRpGVC5eGQcSg4hXLml3fdpBeHs8evWyPKX9ruXVqnYCeAHA8IyC9K2kmkAAAAASUVORK5CYII=");
-ms-interpolation-mode: nearest-neighbor;
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
display: block;
width: 16px;
height: 14px;
padding: 0;
}
.window-action-close {
background-position: calc(-3 * 13px - 1px) 0px;
}
.window-action-maximize {
background-position: calc(-1 * 13px - 1px) 0px;
}
.window-action-restore {
background-position: calc(-2 * 13px - 1px) 0px;
}
.window-action-minimize {
background-position: calc(-0 * 13px - 1px) 0px;
}
.window-action-close:enabled:hover:active,
.window-action-close.pressing {
background-position: calc(-3 * 13px - 0px) 1px;
}
.window-action-maximize:enabled:hover:active,
.window-action-maximize.pressing {
background-position: calc(-1 * 13px - 0px) 1px;
}
.window-action-restore:enabled:hover:active,
.window-action-restore.pressing {
background-position: calc(-2 * 13px - 0px) 1px;
}
.window-action-minimize:enabled:hover:active,
.window-action-minimize.pressing {
background-position: calc(-0 * 13px - 0px) 1px;
}
.menus {
background: rgb(192, 192, 192);

File diff suppressed because one or more lines are too long