// ==UserScript==
// @name ReShape
// @namespace thomasa88
// @match https://example.com/*
// @grant none
// @version 1.1.0
// @author Thomas Axelsson
// @license GPL-3.0+
// @description 5/9/2020, 7:54:47 PM
// ==/UserScript==
//
// Copyright (C) 2020 Thomas Axelsson
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
// jQuery license:
//
// Copyright JS Foundation and other contributors, https://js.foundation/
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
SHIFT = 0x1000
CTRL = 0x2000
ALT = 0x4000
reshapeToggle = function () {
//reshape.editMode = !reshape.editMode;
if (reshape.editMode) {
// Need to figure out closing the box vs updating the keyTable vs saving the keyTable
//document.body.removeChild(reshape.reshapeDialog);
} else {
reshape.editMode = true;
document.body.appendChild(reshape.reshapeDialog);
};
}
var reshapeInit = function() {
if (typeof reshapeLoaded === 'undefined') {
reshape = {};
loadKeyTable();
reshape.keymap = new Map();
packKeymap();
reshape.editMode = false;
reshape.remapTarget = undefined;
reshape.reshapeDialog = createDialog();
addToggleButton(document.body, true);
wrapEvents();
reshapeLoaded = true;
} else {
reshapeToggle();
}
}
// TODO: Replace this by checking the event list now and then during run-time?
var reshapeInitWhenStable = function() {
reshapeEventCount = 0;
let timeout = setTimeout(pollStable, 1000);
}
function pollStable() {
let documentEvents = $._data(document, 'events');
console.log("POLL");
if (documentEvents && documentEvents.keydown && documentEvents.keydown.length > 0 &&
documentEvents.keydown.length == reshapeEventCount) {
console.log("STABLE", documentEvents.keydown.length);
reshapeInit();
} else {
console.log("WAIT");
reshapeEventCount = documentEvents.keydown.length;
setTimeout(pollStable, 1000);
}
}
function loadKeyTable() {
let json = localStorage.getItem('reshape-keytable');
if (json == null) {
reshape.keyTable = [];
} else {
reshape.keyTable = JSON.parse(json);
}
}
function storeKeyTable() {
localStorage.setItem('reshape-keytable', JSON.stringify(reshape.keyTable));
}
function addToggleButton(target) {
let toggleDiv = reshapeLogo(true);
toggleDiv.style.cursor = 'pointer';
toggleDiv.onclick = reshapeToggle;
target.appendChild(toggleDiv);
}
var reshapeLogo = function(dock) {
let logoDiv = document.createElement('div');
logoDiv.innerHTML = '↬';
let toggleStyle = logoDiv.style;
if (dock) {
toggleStyle.position = 'fixed';
toggleStyle.bottom = 0;
toggleStyle.right = 0;
} else {
toggleStyle.margin = '5px';
toggleStyle.display = 'inline-block';
}
toggleStyle.background = 'red';
toggleStyle.fontSize = '20px';
toggleStyle.clipPath = 'circle()';
toggleStyle.width = '20px';
toggleStyle.height = '20px';
toggleStyle.textAlign = 'center';
toggleStyle.lineHeight = '16px';
return logoDiv;
}
function logKey(event) {
console.log(event.which, event.keyCode,
event.shiftKey, event.key,
String.fromCharCode(event.which));
}
function packKeyinfo(keyinfo) {
return keyinfo.keycode | (keyinfo.shift ? SHIFT : 0) | (keyinfo.ctrl ? CTRL : 0) | (keyinfo.alt ? ALT : 0);
}
function packKeyevent(event) {
return event.which | (event.shiftKey ? SHIFT : 0) | (event.ctrlKey ? CTRL : 0) | (event.altKey ? ALT : 0);
}
function unpackKeyinfo(packed) {
return { 'shift': (packed & SHIFT != 0),
'ctrl': (packed & CTRL != 0),
'alt': (packed & ALT != 0),
'keycode': packed & ~SHIFT & ~CTRL & ~ALT};
}
function packKeymap() {
reshape.keymap.clear();
for (let entry of reshape.keyTable) {
if (entry.old.keycode != null && entry.new.keycode != null) {
reshape.keymap.set(packKeyinfo(entry.old), entry);
}
}
}
function wrap(e, handler) {
// NOTE: This function is called for each handler for each key press.
//e.keyCode=83;e.which=83;e.key="s";
let packed = packKeyevent(e);
if (!reshape.editMode) {
let keyinfo = reshape.keymap.get(packed);
if (keyinfo) {
console.log((e.shiftKey ? 'shift+' : '' ) + e.key + '(' + e.which + ') -> ' + (keyinfo.new.shift ? 'shift+' : '' ) + keyinfo.new.keysym + ' (' + keyinfo.new.keycode + ')');
e.which = keyinfo.new.keycode;
e.shiftKey = keyinfo.new.shift;
/* !! for backwards-compatibility to handle keyinfos without the fields */
e.ctrlKey = !!keyinfo.new.ctrl;
e.altKey = !!keyinfo.new.alt;
//logKey(e)
}
handler(e);
if (keyinfo && keyinfo.clickSelector) {
setTimeout(() => {
let clickTarget = document.querySelector(keyinfo.clickSelector);
if (clickTarget) {
clickTarget.click()
} else {
console.log("No match for click selector:",
keyinfo.clickSelector);
}}, 100);
}
} else {
if (reshape.remapTarget &&
e.which != 16 /*shift key */ &&
e.which != 18 /* altgr */ &&
e.which != 17 /* ctrl */ &&
!e.shiftKey /* We only want the keysym from the non-shifted key */ &&
!e.ctrlKey &&
!e.altKey) {
keyinfo = { 'shift': e.shiftKey, 'ctrl': e.ctrlKey, 'alt': e.altKey, 'keycode': e.which, 'keysym': e.key };
reshape.remapTarget(keyinfo);
reshape.remapTarget = undefined;
}
}
}
function wrapEvents() {
let documentEvents = $._data(document, 'events');
documentEvents.keydown.forEach((jevent) => {
let h = jevent.handler;
jevent.handler = (e => wrap(e, h));
});
}
function keySpan(text) {
let span = document.createElement('span');
span.innerText = text;
span.style.cursor = 'pointer';
span.style.border = '1px solid gray';
span.style.backgroundColor = '#eeeeee';
span.style.fontSize = '10px';
span.style.height = '16px';
span.style.lineHeight = '16px';
span.style.padding = '0px 5px 0px 5px';
span.style.display = 'inline-block';
span.style.textAlign = 'center';
return span;
}
function modifierSpan(name, keyinfo, member) {
let span = keySpan(name);
if (!keyinfo[member]) {
span.style.opacity = '0.3';
}
span.onclick = e => toggleModifier(keyinfo, member, span);
return span;
}
function keyCell(cell, keyinfo) {
cell.appendChild(modifierSpan('ctrl', keyinfo, 'ctrl'));
cell.appendChild(modifierSpan('alt', keyinfo, 'alt'));
cell.appendChild(modifierSpan('shift', keyinfo, 'shift'));
let basekeySpan = keySpan(keyText(keyinfo));
// E.g. "Escape" takes up more space than one letter
//basekeySpan.style.width = '20px';
cell.appendChild(basekeySpan);
basekeySpan.onclick = e => startRemappingKey(keyinfo, basekeySpan);
basekeySpan.oncontextmenu = e => remapKeyWithCode(keyinfo, basekeySpan);
}
function keyText(keyinfo) {
if (keyinfo.keysym == null) {
return '';
}
//return String.fromCharCode(keyinfo.keycode);
let text = keyinfo.keysym;
if (keyinfo.keysym == ' ') {
text = 'Space';
} else if (keyinfo.keysym.length == 1) {
/* One letter */
text = keyinfo.keysym.toUpperCase();
}
return text + ' <' + keyinfo.keycode + '>';
}
function toggleModifier(keyinfo, member, span) {
keyinfo[member] = !keyinfo[member];
if (keyinfo[member]) {
span.style.opacity = '1';
} else {
span.style.opacity = '0.3';
}
}
function startRemappingKey(keyinfo, basekeySpan) {
basekeySpan.innerHTML = ' ';
reshape.remapTarget = function(pressKeyinfo) {
reshape.remapTarget = undefined;
keyinfo.keycode = pressKeyinfo.keycode;
keyinfo.keysym = pressKeyinfo.keysym;
basekeySpan.innerText = keyText(keyinfo);
}
}
function remapKeyWithCode(keyinfo, basekeySpan) {
reshape.remapTarget = undefined;
let origKeycode = keyinfo.keycode;
if (origKeycode === null) {
origKeycode = '';
}
while (true) {
let answer = prompt("Enter the keycode", origKeycode);
if (!answer) {
break;
}
let keycode = parseInt(answer);
if (isNaN(keycode)) {
alert("Keycode must be a number");
continue;
}
keyinfo.keycode = keycode;
keyinfo.keysym = '';
basekeySpan.innerText = keyText(keyinfo);
break;
}
return false;
}
function changeValue(entry, member, input) {
entry[member] = input.value;
}
function addRow(tbody, entry) {
let r = tbody.insertRow(-1);
r.style.lineHeight = '25px';
let noteCell = r.insertCell(-1);
noteCell.style.display = 'inline-flex';
let noteText = document.createElement('input');
noteText.type = 'text';
noteText.style.lineHeight = '10px';
noteText.style.width = '100px';
if (entry.note) {
noteText.value = entry.note;
}
noteText.onchange = () => changeValue(entry, 'note', noteText);
noteCell.appendChild(noteText);
let oldCell = r.insertCell(-1);
keyCell(oldCell, entry.old);
let newCell = r.insertCell(-1);
keyCell(newCell, entry.new);
let clickCell = r.insertCell(-1);
clickCell.style.display = 'inline-flex';
let clickText = document.createElement('input');
clickText.type = 'text';
clickText.style.lineHeight = '10px';
clickText.style.width = '100px';
if (entry.clickSelector) {
clickText.value = entry.clickSelector;
}
clickText.onchange = () => changeValue(entry, 'clickSelector', clickText);
clickCell.appendChild(clickText);
let removeCell = r.insertCell(-1);
// Center button vertically
removeCell.style.display = 'inline-flex';
let removeButton = document.createElement('input');
removeButton.type = 'button';
removeButton.value = 'Remove';
removeButton.style.lineHeight = 'normal';
removeCell.appendChild(removeButton);
removeButton.onclick = function(e) {
let pos = reshape.keyTable.indexOf(entry);
reshape.keyTable.splice(pos, 1);
tbody.removeChild(r);
};
return r;
}
function rawEdit() {
let answer = prompt("Copy/paste. Note that inserting bad data will make ReShape work incorrectly. Settings are not saved until you click 'Save'.",
JSON.stringify(reshape.keyTable));
if (answer) {
reshape.keyTable = JSON.parse(answer);
populateDialogTable();
}
}
function createDialog() {
d=document.createElement('div');
d.appendChild(reshapeLogo());
let titleSpan = document.createElement('span');
titleSpan.innerHTML = 'ReShape 1.1.0';
titleSpan.style.fontSize = '16px';
d.appendChild(titleSpan);
d.id = 'reshape-form'
d.style.position = 'fixed';
d.style.marginLeft = 'auto';
d.style.marginRight = 'auto';
d.style.top = '100px';
d.style.backgroundColor = 'white';
d.style.border = '1px silver solid';
d.style.padding = '5px';
// Need space for keys such as "ArrowDown"
d.style.width = '700px';
d.style.height = '400px';
tableDiv = document.createElement('div');
tableDiv.style.height = '300px';
tableDiv.style.overflowY = 'auto';
tableDiv.style.marginBottom = '15px';
tableDiv.style.marginTop = '10px';
reshape.dialogTable=document.createElement('table');
let thead = document.createElement('thead');
header = thead.insertRow(-1);
header.style.position = 'sticky';
header.style.top = '0px';
header.style.backgroundColor = 'white';
header.style.zIndex = 10;
header.style.height = '2em';
header.innerHTML = 'Note | Listen for keys | Send keys | & Click on (CSS selector) | | ';
reshape.dialogTable.appendChild(thead);
populateDialogTable();
tableDiv.appendChild(reshape.dialogTable);
d.appendChild(tableDiv)
addButton = document.createElement('input');
addButton.type = 'button';
addButton.value = 'Add Key';
addButton.onclick = function(e) {
let entry = {
'old': { 'shift': false, 'ctrl': false, 'alt': false, 'keycode': null, 'keysym': null },
'new': { 'shift': false, 'ctrl': false, 'alt': false, 'keycode': null, 'keysym': null }
};
reshape.keyTable.push(entry)
addRow(reshape.dialogTable.tBodies[0], entry).scrollIntoView();
}
d.appendChild(addButton);
tipText = document.createTextNode(' Right click on keys to set custom keycode. ')
d.appendChild(tipText);
rawButton = document.createElement('input');
rawButton.type = 'button';
rawButton.value = 'Edit Raw';
rawButton.onclick = rawEdit;
d.appendChild(rawButton);
closeButton = document.createElement('input');
closeButton.type = 'button';
closeButton.value = 'Save & Close';
closeButton.style.float = 'right';
closeButton.onclick = function(e) {
document.body.removeChild(d);
storeKeyTable();
packKeymap();
reshape.editMode = false;
}
d.appendChild(closeButton);
return d;
}
function populateDialogTable() {
if (reshape.dialogTable.tBodies.length > 0) {
reshape.dialogTable.removeChild(reshape.dialogTable.tBodies[0]);
}
let tbody = document.createElement('tbody');
reshape.dialogTable.appendChild(tbody);
for (let entry of reshape.keyTable) {
addRow(tbody, entry);
}
}
reshapeInitWhenStable();