Сделал отдельный скрипт на основе кнопки от Dumby «Быстрые опции about:config»: (меньше оригинала + некоторые улучшения)
Dumby, раз ты на связи, вот мои Хотелки и Проблемы к скрипту Быстрые опции:
1) сделать ввод значения текущей опции вручную через диалог ввода. Например в UserAgent > при клике по строке: "ваши данные…" нужно открыть диалог, где вводим нужные данные аналогично уже вшитым в другие строки этого подменю. После ввода или перезапуска браузера при открытии этого подменю строка "ваши данные…" должна быть выбрана флажком. Такой ввод данных пользователя нужен не для одной опции, а для многих: например в строке «Загрузки» нужна строка своих настроек…
2) надо, чтобы radio-строка подменю была выбрана (с флажком), если опция сброшена и отсутствует.
сделал костыль: pref.undef = [val,str] для pref: pref,lab,key,hint,[val,str],code
костыль делает строку меню такой: "Заголовок - str", но radio-строка подменю остаётся не выбранной.
3) при клике по родительской строке меню открывать эту опцию в about:config – функция about_config прилагается.
Это работает в ucf_hookClicks.js, но не пашет в этом коде, т.к. e.target определяется как кнопка, а не как текущая строка меню.
4) оригинальная кнопка "Быстрое переключение опций about:config" содержала два контекстных меню.
Прошу убрать код для нескольких меню: popups, popupshowing… (может попроще код станет).
Второе UserMenu другого формата и должно быть независимым, чтобы вызываться над разными кнопками.
0) странноcти кода – добавлял функцию switchTab через запятую в разной последовательности, но не
работает: ua = …}, switchTab = … Без запятой пашет: строка UserMenu не выдаст ошибку: no switchTab…
/* Быстрые настройки меню опций about:config. Колёсико, Лев+Shift не закрывать Опция изменена: курсив; Цвет = ключ, по-умолчанию серый, иначе Red есть Def3el: несовпадения выделяются красной обводкой текста refresh=true ⟳ перечитать без кэша, restart=false ↯ без запроса pref: pref,lab,key,hint,[val,str],code | keys:val,lab,dat,+hint,code | icon:значок предыдущее значение: menu.pref.val Новое: newVal и trg.val данные trg.label метка keys: */ (async id =>{ var {prefs} = Services, db = prefs.getDefaultBranch(""); Pref = (key, set)=>{ //или key = [key,default] if (!Array.isArray(key)) key = [key]; var t = prefs.getPrefType(key[0]), m = {b:"Bool",n:"Int",s:"String"}; t = m[t == 128 ? "b" : t == 64 ? "n" : t == 32 ? "s" : ""]; if (set == "get") return t; //тип опции if (!t) t = m[set != undefined ? (typeof set)[0] : (typeof key[1])[0]]; if (t) if (set != undefined) prefs[`set${t}Pref`](key[0],set) else set = prefs[`get${t}Pref`](...key); return set; } Icon = (c = '0c0')=>"data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'><defs><linearGradient id='a' x1='16' x2='16' y1='32' gradientUnits='userSpaceOnUse'><stop stop-color='%23"+ c +"'/><stop stop-color='%23fff' offset='.8'/></linearGradient><linearGradient id='b' x2='32' y1='16' gradientTransform='matrix(1 0 0 1 2 2)'><stop stop-opacity='.5'/></linearGradient></defs><circle cx='16' cy='16' r='15' fill='url(%23a)' stroke='url(%23b)' stroke-width='2'/></svg>"; about_config = (filter) => { //на опцию if (gURLBar.value.startsWith("about:config")) switchTab(gURLBar.value); var setFilter = (e,input = (e?.target || window.content.document).getElementById("about-config-search")) => { try { if (e || input.value != filter) input.setUserInput(filter);} catch{} }, found = window.switchToTabHavingURI("about:config",true, {relatedToCurrent: true, triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()}); if (found) setFilter(null,window); else gBrowser.selectedBrowser.addEventListener("pageshow",setFilter, {once: true}); }; ua = (real = false, ua_my = "general.useragent.override")=>{ //текущий или вшитый ЮзерАгент ttt = Pref(ua_my); prefs.clearUserPref(ua_my); ua = Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).userAgent; //костыль ttt && Pref(ua_my, ttt); ttt ||= ua; if (real) ttt = ua; return ttt; } switchTab = (url = 'about:config', go)=>{ //открыть вкладку | закрыть её | выбрать for(var tab of gBrowser.visibleTabs) if (tab.linkedBrowser.currentURI.spec == url) {go ? gBrowser.selectedTab = tab : gBrowser.removeTab(tab); return;} gBrowser.addTrustedTab(url); gBrowser.selectedTab = gBrowser.visibleTabs[gBrowser.visibleTabs.length -1]; } var uar = ua(true), //real ЮзерАгент fonts = arr => arr.map(n => [(n == arr[arr.length-1] ? null : n), n]), //array с вложениями serif = fonts("Arial|Roboto|Cantarell|Segoe UI|Cambria|Calibri".split('|')), sans = [["Times","Times"], ...serif], hints = new Map([ //опция отсутствует ? вернуть строку ["general.useragent.override", "UserAgent"]]); UserMenu = { //массив команд пользователя, alt() клик правой кнопкой "Настройки профайлера": { cmd(){switchTab('about:profiling')} }, DwDir: {lab: `папка Загрузки`, cmd(){ Downloads.getSystemDownloadsDirectory().then(path => FileUtils.File(path).launch(),Cu.reportError)}, img: "chrome://devtools/skin/images/folder.svg" }, } AboutCfg = [{ pref: ["dom.disable_open_during_load", "Всплывающие окна"], Def3el: true, Yellow: false, keys: [[true, "Блокировать"], [false, "Разрешить"]], },null,{ pref: ["extensions.user_chrome_files.savedirs", "Загрузки",,'Пути сохранения Страниц и Графики\nСинтаксис «_Html/subdir|_Pics/subdir»\nsubdir: пусто | 0 заголовок | 1 домен', ["", "всё в общей папке"]], Blue: "_Web|1|_Images|0", Gray: "", keys: [ ["_Сайты||_Фото|0", "_Сайты|_Фото/имя…"], ["_Web|1|_Images|0", "_Web/сайт|_Images/имя"], ["Сайт||Фото|", "ваши данные…",,"ключ в about:config", `console.log("введите ваши данные…")`]] },{ pref: ["general.useragent.override", "User Agent",,"Изменяет вид сайта", [uar,"встроенный"]], refresh: true, Def3el: uar, keys: [ ["Windows", "ваши данные…",,,`console.log("введите ваши данные…")`], ["Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", "Chrome 118 Win10"], ["Opera/9.80 (Windows NT 6.2; Win64; x64) Presto/2.12 Version/12.16", "Opera12 W8"], ["Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "GoogleBot"], [uar, "по-умолчанию"]] //родной ЮзерАгент }]; CustomizableUI.createWidget({ defaultArea: CustomizableUI.AREA_NAVBAR, label: "Журнал, Меню опций", localized: false, id: id, onCreated(btn) { btn.setAttribute("image", "chrome://devtools/skin/images/settings.svg"); var doc = btn.ownerDocument, m = nn => doc.createXULElement(nn); btn.domParent = null; btn.popups = new btn.ownerGlobal.Array(); this.createPopup(doc, btn, "config", AboutCfg); btn.linkedObject = this; for(var type of ["contextmenu", "command"]) btn.setAttribute("on"+ type, `linkedObject.${type}(event)`); var popup = m("menupopup"), menu = m("menuitem"); menu.m = m; menu.fill = this.fill; menu.render = this.render; popup.append(menu); btn.prepend(popup); }, render(){ var popup = this.parentNode; this.remove(); this.fill(UserMenu, popup); }, fill(o, popup){if (typeof o == "object") for (key in o){ var {lab, sep, sub, cmd, alt, inf, img} = o[key]; var name = sub ? "menu" : "menuitem"; sep && popup.append(this.m("menuseparator")); var item = this.m(name); item.setAttribute("label", lab || key); if (img) item.className = name +"-iconic", item.setAttribute("image",img); if (inf) item.tooltipText = inf; item.alt = alt; //cmd2 sub || cmd && item.setAttribute("oncommand", cmd.toString().replace(/cmd\(.*?\)/,'')); /^(sub|sep|inf|lab|img)$/.test(key) || popup.append(item); sub && this.fill(o[key], item.appendChild(this.m("menupopup"))); }}, createPopup(doc, btn, prop, data) { var popup = doc.createXULElement("menupopup"); btn.popups.push(btn[prop] = popup); popup.id = this.id +"-"+ prop; for (var type of ["popupshowing"]) popup.setAttribute("on"+ type, `parentNode.linkedObject.${type}(event)`); for(var obj of data) popup.append(this.createElement(doc, obj)); btn.append(popup); }, createElement(doc, obj) { //pref if (!obj) return doc.createXULElement("menuseparator"); var pref = doc.ownerGlobal.Object.create(null), node, bool, img; for(var [key, val] of Object.entries(obj)) { if (key == "pref") { var [apref, lab, akey, hint, undef, code] = val; //строка меню pref.pref = apref; pref.lab = lab || apref; if (hint) { if (RegExp(/\p{L}/,'u').test(hint[0]) && (hint[0] === hint[0].toUpperCase())) hint = '\n'+ hint; pref.hint = hint; } if (undef) pref.undef = undef; //если не массив: undef || undef == "" if (code) pref.code = code; } else if (key == "icon") img = val, pref.img = true; else if (key != "keys") pref[key] = val; else pref.hasVals = true; } var t = prefs.getPrefType(pref.pref), m = {b: "Bool", n: "Int", s: "String"}; var str = m[t == prefs.PREF_INVALID ? obj.keys ? (typeof obj.keys[0][0])[0] : "b" : t == prefs.PREF_BOOL ? "b" : t == prefs.PREF_INT ? "n" : "s"]; //String по-умолчанию pref.get = prefs[`get${str}Pref`]; var map, set = prefs[`set${str}Pref`]; if (pref.hasVals) { for(var [val,,,,code] of obj.keys) code && (map || (map = new Map())).set(val, code); if (map) pref.set = (key, val) => { set(key, val); map.has(val) && eval(map.get(val)); //код2 если pref изменён } } if (!map) pref.set = set; node = doc.createXULElement("menu"); node.className = "menu-iconic"; img && node.setAttribute("image", img); akey && node.setAttribute("accesskey", akey); (node.pref = pref).vals = doc.ownerGlobal.Object.create(null); this.createRadios(doc, str.startsWith("B") && !pref.hasVals ? [[true, "true"], [false, "false"]] : obj.keys, node.appendChild(doc.createXULElement("menupopup")) ); if ("Def3el" in obj) pref.noAlt = !("Yellow" in obj); return node; }, createRadios(doc, vals, popup) { for(var arr of vals) { var [val, lab, key, hint] = arr; var menuitem = doc.createXULElement("menuitem"); with (menuitem) setAttribute("type","radio"), setAttribute("closemenu","none"), setAttribute("label", popup.parentNode.pref.vals[val] = lab), key && setAttribute("accesskey", key); var tip = menuitem.val = val === "" ? "[ пустая строка ]" : val; if (hint) tip += "\n" + hint; menuitem.tooltipText = `${tip != undefined ? tip + "\n\n" : ""}клик с Shift блокирует авто-закрытие`; popup.append(menuitem); } }, regexpRefresh: /^(?:view-source:)?(?:https?|ftp)/, upd(node) { var {pref} = node, def = false, user = false, val; //если опция не найдена if (prefs.getPrefType(pref.pref) != prefs.PREF_INVALID) { try { val = pref.defVal = db[pref.get.name](pref.pref); def = true; //опция по-умолчанию получена } catch {def = false} user = prefs.prefHasUserValue(pref.pref); if (user) try {val = pref.get(pref.pref, undefined);} catch {} } if (val == pref.val && def == pref.def && user == pref.user) return; pref.val = val; pref.def = def; pref.user = user; var exists = def || user; if (!exists && pref.undef) //опции нет ? вернуть default val = pref.undef[0]; var hint = hints.get(pref.pref); hint ||= val != undefined ? val : "Эта опция не указана"; if (hint === "") hint = "[ пустая строка ]"; hint += "\n" + pref.pref; if (pref.hint) hint += "\n"+ pref.hint; node.tooltipText = hint; //+ текст var img = Icon("999"), alt = "Yellow" in pref && val == pref.Yellow, clr = "Gray" in pref && val == pref.Gray, blu = "Blue" in pref && val == pref.Blue; if (blu) img = Icon("a0f"); if (alt) img = Icon("f80"); if ("Def3el" in pref) if (val == pref.Def3el) img = Icon(), node.style.removeProperty('filter'); else if (val != pref.defVal) { if (!alt && !clr && !blu) img = Icon("f26"); // Red node.style.filter = "drop-shadow(1px 1px 1px #B8F)"; } pref.img || node.setAttribute("image", img); //нет Def3el ? серый user ? node.style.setProperty("font-style", "italic", "important") : node.style.removeProperty("font-style"); var {lab} = pref; if (exists && pref.hasVals) { if (val in pref.vals) var sfx = pref.vals[val] || val; else var sfx = user ? "другое" : "по-умолчанию"; lab += ` ${"restart" in pref ? "↯-" : "refresh" in pref ? "-⟳" : "—"} ${sfx}`; } lab = exists ? lab : '['+ lab + `${"restart" in pref ? " ↯" : "refresh" in pref ? " ⟳" : ""}` +']'+ `${pref.undef ? " - "+ pref.undef[1] : ""}`; node.setAttribute("label", lab); //имя = [имя] если преф не существует }, openPopup(popup) { var btn = popup.parentNode; if (btn.domParent != btn.parentNode) { btn.domParent = btn.parentNode; if (btn.matches(".widget-overflow-list > :scope")) var pos = "after_start"; else var win = btn.ownerGlobal, {width, height, top, bottom, left, right} = btn.closest("toolbar").getBoundingClientRect(), pos = width > height ? `${win.innerHeight - bottom > top ? "after" : "before"}_start` : `${win.innerWidth - right > left ? "end" : "start"}_before`; for(var p of btn.popups) p.setAttribute("position", pos); } popup.openPopup(btn); }, maybeRestart(node, conf) { if (conf && !Services.prompt.confirm(null, this.label, "Перезапустить браузер?")) return; var cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); Services.obs.notifyObservers(cancel, "quit-application-requested", "restart"); return cancel.data ? Services.prompt.alert(null, this.label, "Запрос на выход отменён.") : this.restart(); }, async restart() { var meth = Services.appinfo.inSafeMode ? "restartInSafeMode" : "quit"; Services.startup[meth](Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); }, maybeRe(node, fe) { var {pref} = node; if ("restart" in pref) { if (this.maybeRestart(node, pref.restart)) return; } else this.popupshowing(fe, node.parentNode); if ("refresh" in pref) { var win = node.ownerGlobal; if (this.regexpRefresh.test(win.gBrowser.currentURI.spec)) pref.refresh ? win.BrowserReloadSkipCache() : win.BrowserReload();} }, maybeClosePopup(e, trg) { (e.shiftKey || e.button == 1) || trg.parentNode.hidePopup(); }, popupshowing(e, trg = e.target) { if (trg.state == "closed") return; if (trg.id) { for(var node of trg.children) { if (node.nodeName.endsWith("r")) continue; this.upd(node); !e && node.open && this.popupshowing(null, node.querySelector("menupopup")); } return; } var {pref} = trg.closest("menu"), findChecked = true; var findDef = "defVal" in pref; var checked = trg.querySelector("[checked]"); if (checked) { if (checked.val == pref.val) { if (findDef) findChecked = false; else return; } else checked.removeAttribute("checked"); } if (findDef) { var def = trg.querySelector("menuitem:not([style*=font-style]"); if (def) if (def.val == pref.defVal) { if (findChecked) findDef = false; else return; } else def.style.setProperty("font-style","italic","important"); } for(var node of trg.children) if ("val" in node) { if (!pref.val && pref.val != "" && pref.undef) pref.val = pref.undef[0]; //опции нет ? вернуть default if (findChecked && node.val == pref.val) { node.setAttribute("checked", true); if (findDef) findChecked = false; else break; } if (findDef && node.val == pref.defVal) { node.style.removeProperty("font-style"); if (findChecked) findDef = false; else break; } } }, command(e, trg = e.target) { //LMB if (trg.id == id) { if (trg.config.state != "open") trg.menupopup.openPopup(trg, "after_start") else console.log("Здесь нужен переход на опцию about:config"); return; } var menu = trg.closest("menu"), newVal = trg.val; if (!menu || !menu.pref) return; this.maybeClosePopup(e, menu); menu.pref.code && eval(menu.pref.code); //run1 if (newVal != menu.pref.val) menu.pref.set(menu.pref.pref, newVal), this.maybeRe(menu, true); }, contextmenu(e, trg = e.target) { //RMB if ((trg.id == id) && (!e.ctrlKey && !e.altKey && !e.shiftKey)) if (trg.config.state != "open") this.openPopup(trg.config); else trg.config.hidePopup(); else if ("pref" in trg) { this.maybeClosePopup(e, trg); if (trg.pref.user) prefs.clearUserPref(trg.pref.pref), this.maybeRe(trg); trg.pref.code && eval(trg.pref.code); //run } e.preventDefault(); } }); })("ucf_AboutConfig");
Отсутствует
1) сделать ввод значения текущей опции вручную через диалог ввода. Например в UserAgent > при клике по строке: "ваши данные…" нужно открыть диалог, где вводим нужные данные аналогично уже вшитым в другие строки этого подменю. После ввода или перезапуска браузера при открытии этого подменю строка "ваши данные…" должна быть выбрана флажком.
Попробовал это записать, но как-бы слегка снаружи.
Задержка, и, если ничего не выбрано и преф имеет пользовательское значение,
тогда выбираем строку "ваши данные…".
Врезка в createRadios() перед строкой popup.append(menuitem);
добавляем lab == "ваши данные…" && this.asInput(menuitem, val);
дополнительный стафф в объект
// asInput(menuitem, val) { menuitem.ttsfx = (menuitem.ittt = menuitem.tooltipText) .slice(String(menuitem.ival = val).length); menuitem.linkedObject = this; menuitem.setAttribute("oncommand", "linkedObject.input(event)"); menuitem.render = this.renderInputOnce; }, input(e) { e.stopImmediatePropagation(); var trg = e.target, {pref} = trg.parentNode.parentNode; var res = {value: trg.tt ? pref.val : trg.ival}; if (Services.prompt.prompt( null, `Введите значение (${pref.isInt ? "целое" : "строка"})`, pref.pref, res, null, {} )) { if (pref.isInt) { // from old about:config var val = res.value | 0; if (val != res.value - 0) return Services.prompt.alert( null, "Недействительное значение", "Введённый вами текст не является числом." ); } else val = res.value; pref.set(pref.pref, val); } }, renderInputOnce() { delete this.render; this.render(); var {pref} = this.parentNode.parentNode; pref.isInt = pref.get.name[3] == "I"; if (!("val" in pref)) try {pref.val = pref.get(pref.pref);} catch {} this.linkedObject.hookUncheck(this); // for Shif+Click outside (this.render = this.linkedObject.renderInput).call(this); }, hookUncheck(menuitem) { var rtt = function() { this.tt = false; this.tooltipText = this.ittt; } var desc = {configurable: true, enumerable: true, value(attr) { this.ownerGlobal.Element.prototype.removeAttribute.call(this, attr); attr.startsWith("c") && this.popup.state.startsWith("o") && this.parentNode.rtt(); }}; (this.hookUncheck = menuitem => { menuitem.rtt = rtt; var box = menuitem.firstChild; box.popup = menuitem.parentNode; menuitem.ownerGlobal.Object.defineProperty(box, "removeAttribute", desc); })(menuitem); }, get renderInput() { delete this.renderInput; return this.renderInput = Cu.getGlobalForObject(Cu).eval(`(${async function() { await new Promise(this.ownerGlobal.requestAnimationFrame); var popup = this.parentNode, menu = popup.parentNode; menu.hasAttribute("_moz-menuactive") || this.linkedObject.popupshowing(null, popup, false); // force var checked = popup.querySelector("[checked]"); var {pref} = menu, checkedThis = checked == this; if (checked && !checkedThis || !pref.user) return this.tt && this.rtt(); this.tt = true; this.tooltipText = pref.val + this.ttsfx; if (checkedThis) return; menu.label = menu.label.replace(/другое$/, this.label); this.setAttribute("checked", true); }})`); },
/* popupshowing(e, trg = e.target) { if (trg.state == "closed") return; */ popupshowing(e, trg = e.target, checkstate = true) { if (checkstate && trg.state == "closed") return;
2) надо, чтобы radio-строка подменю была выбрана (с флажком), если опция сброшена и отсутствует.
сделал костыль: pref.undef = [val,str] для pref: pref,lab,key,hint,[val,str],code
костыль делает строку меню такой: "Заголовок - str", но radio-строка подменю остаётся не выбранной.
Не понял. Какая radio-строка остаётся не выбранной?
Вот для extensions.user_chrome_files.savedirs есть pref.undef
и есть три radio-строки, но ни одна из них
не подходит быть выбранной если преф отсутствует.
И, даже если бы подходящая radio-строка была,
то что должно было бы происходить при её активации кликом?
3) при клике по родительской строке меню открывать эту опцию в about:config – функция about_config прилагается.
Это работает в ucf_hookClicks.js, но не пашет в этом коде, т.к. e.target определяется как кнопка, а не как текущая строка меню.
У себя вижу, что на кликнутое ссылается e.explicitOriginalTarget
4) оригинальная кнопка "Быстрое переключение опций about:config" содержала два контекстных меню.
Прошу убрать код для нескольких меню: popups, popupshowing… (может попроще код станет).
Да нет, особо попроще код не станет. В popupshowing здесь менять нечего,
а popups — да, не используется, можно почистить
.... //btn.popups = new btn.ownerGlobal.Array(); .... //btn.popups.push(btn[prop] = popup); btn[prop] = popup; .... //for(var p of btn.popups) p.setAttribute("position", pos); btn.config.setAttribute("position", pos);
0) странноcти кода – добавлял функцию switchTab через запятую в разной последовательности, но не
работает: ua = …}, switchTab = … Без запятой пашет: строка UserMenu не выдаст ошибку: no switchTab…
Я добавил запятую как написано, и у меня всё работает.
Запятая, в данном случае, это оператор множественного вычисления,
она что есть, что нет.
А вот если перед ua добавить var
то запятая станет частью var-синтаксиса
и switchTab перестанет быть глобальной переменной.
То есть, из атрибута "oncommand" его видно не будет.
Отсутствует
Можно в ucf_aom-button.js как-то добавить функцию, как у unified_extensions_button, чтобы при клике вылазило окно, не настройки? Можно эти дополнения с меню, отдельно выделять используя тот шарик с цветом. Во, или на ПКМ посадить unified_extensions_button, а саму кнопку скрыть.
Отредактировано b0ttle (15-12-2023 17:55:26)
Отсутствует
Можно эти дополнения с меню, отдельно выделять используя тот шарик с цветом.
Не понял, нужен скриншот или подробные разьяснения.
Сделал, но пока не публиковал (ucf_hookClicks.js в процессе отладки):
Дополнения к меню опций от Dumby (работает, но переделываю по-своему, ещё изменю формат Setup = [{…)
Меню пользователя и прочие клики для unified_extensions_button работают и на старой кнопке Расширений "add-ons-button"
Можно в ucf_aom-button.js как-то добавить функцию, как у unified_extensions_button, чтобы при клике вылазило окно, не настройки?
Вообще-то удобнее иконку скрипта ucf_aom-button.js убрать, а на правый клик unified_extensions_button назначить меню управления расширениями от ucf_aom-button (но не разбирался, как в этом скрипте привязать клики по меню, открытому из другой кнопки).
Отредактировано Dobrov (16-12-2023 06:30:12)
Отсутствует
попап таких расширений как ublock, tampermonkey, про это меню имелось ввиду, чтобы вместо двух кнопок была одна.
Нет, менюшки расширений не рассматриваются, и я не проверял их открытие над другой кнопкой, если кнопка расширения не закреплена на панели инструментов.
Правым кликом на unified-extensions-button открывается меню пользовательских команд (отлажу скрипт и для "add-ons-button" это меню добавлю)
Отсутствует
Обновил скрипта перехвата кликов-нажатий ucf_hookClicks.js — исправил ошибки несрабатывания некоторых команд пользовательского меню по клику из кнопок.
Cкрипт сохранения страниц SingleHTML.jsm исправлена связка Путь сохранения <-> установка пути из Опций быстрых настроек.
Всех с наступающим праздником!
Отсутствует
b0ttle
Спасибо, помогло
я у себя их просто уменьшил, чтобы не такие огромные были.
#titlebar-buttonbox > .titlebar-button, .titlebar-buttonbox > :-moz-any(.titlebar-min,.titlebar-max,.titlebar-close,.titlebar-restore), #minimize-button, #restore-button, #close-button {
margin: 0 !important;
padding: 4px 8px !important;
}
Всех с праздником, наступившим и наступающим!
Отредактировано Northtech (31-12-2023 19:39:18)
Отсутствует
в новой версии ucf_hookClicks.js пропадает блок кнопок закрытия окна (#minimize-button, #restore-button, #close-button).
Попробуй включить "Кнопки управления окна" в диалоге "Настройки UserChromeFiles"
Отсутствует
Dumby посмотрите пожалуйста эти две кнопки первая не работает
//Восстановить фавиконки закладок (async () => { var id = "ucf-loads-favicons", label = "Восстановить фавиконки", tooltiptext = "Восстановить фавиконки закладок", img = (rph => { var subst = "ucf-loads-favicons-btn-img"; rph.setSubstitution(subst, Services.io.newURI( "data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16'><path style='fill:none;stroke:context-fill rgb(142, 142, 152);stroke-opacity:context-fill-opacity;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round;' d='M3.6.6v14.8L8 11l4.4 4.4V.6z'/></svg>" )); return `resource://${subst}/`; })(Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler)); maxrequests = 50, // Максимальное количество параллельных запросов maxtimeout = 30, // Длительность до прерывания запроса в секундах alertnotification = true; // Уведомление о завершении поиска фавиконок для закладок var favicons = { _favrunning: false, get alertsService() { delete this.alertsService; return this.alertsService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); }, showAlert(title, val) { try { this.alertsService.showAlertNotification(img, title, val, false); } catch(e) {} }, favSearchStart() { if (this._favrunning) return; this._favrunning = true; this.callWithEachWindow(id, {fill: "color-mix(in srgb, currentColor 20%, #e31b5d)"}); PlacesUtils.promiseBookmarksTree(PlacesUtils.bookmarks.rootGuid).then(root => { var urlsList = []; var convert = (node, url) => { if (node.children) node.children.map(convert); else if ((url = node.uri) && /^(?:https?|ftp|file):/.test(url)) urlsList.push(url); }; convert(root); var favForPage = siteURI => { return new Promise(resolve => { try { siteURI = Services.io.newURI(siteURI); } catch(e) { resolve(null); } PlacesUtils.favicons.getFaviconURLForPage(siteURI, uri => { if (uri === null) resolve(siteURI); else resolve(null); }); }); }; Promise.all(urlsList.map(favForPage)).then(results => this.favSearchResults(results.filter(url => url !== null))); }); }, favComplete(favsuccesslength, favmaxlength) { this._favrunning = false; this.callWithEachWindow(id, {fill: ""}); if (alertnotification) this.showAlert("Поиск фавиконок", `Успешно обработано - ${favsuccesslength}, не удалось обработать - ${favmaxlength - favsuccesslength}`); }, favSearchResults(results) { var favmaxlength = results.length; var favsuccesslength = 0; if (!favmaxlength) { this.favComplete(0, 0); return; } var favmaxtimeout = maxtimeout * 1000; var _favmaxlength = favmaxlength; var splice = results.splice(0, maxrequests); var favSearchPage = siteURI => { (new Promise(resolve => { try { let req = new XMLHttpRequest(); req.mozBackgroundRequest = true; req.open("GET", siteURI.spec, true); req.responseType = "document"; req.overrideMimeType("text/html"); req.timeout = favmaxtimeout; req.onload = () => {console.log(req) try { let doc = req.responseXML, favURI; if (doc) { let links = doc.querySelectorAll("head link[href][rel~='icon']"), lastlink, is16, is32, isany; for (let link of links) { if (link.sizes.length === 1) { let size = link.sizes[0]; if (/any/i.test(size)) isany = link; else if (/32x32/i.test(size)) is32 = link; else if (/16x16/i.test(size)) is16 = link; } lastlink = link; } links = isany || is32 || is16 || lastlink; if (links) favURI = links.href; } if (!favURI) favURI = `${req.responseURL ? Services.io.newURI(req.responseURL).prePath : siteURI.prePath}/favicon.ico`; let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let request = PlacesUtils.favicons.setAndFetchFaviconForPage(siteURI, Services.io.newURI(favURI), false, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, { onComplete() { ++favsuccesslength; resolve(); timer.cancel(); timer = null; request = null; }, }, Services.scriptSecurityManager.getSystemPrincipal()); if (!request) { resolve(); timer = null; return; } timer.initWithCallback(() => { resolve(); try { request.cancel(); } catch(e) {} timer = null; request = null; }, favmaxtimeout, timer.TYPE_ONE_SHOT); } catch(e) { resolve(); } }; req.onabort = () => { resolve(); }; req.onerror = req.ontimeout = () => { resolve(); req.abort(); }; req.send(null); } catch(e) { resolve(); } })).then(() => { if (!(--_favmaxlength)) { this.favComplete(favsuccesslength, favmaxlength); return; } if (!results.length) return; favSearchPage(results.shift()); }); }; splice.map(favSearchPage); }, callWithEachWindow(buttonID, atr) { var getW = CustomizableUI.getWidget(buttonID); if (getW.instances.length) for (let {node} of getW.instances) { if (!node) continue; for (let a in atr) node.style.setProperty(a, atr[a]); } else for (let win of CustomizableUI.windows) { let node = getW.forWindow(win).node; if (!node) continue; for (let a in atr) node.style.setProperty(a, atr[a]); } }, }; CustomizableUI.createWidget({ id: id, label: label, tooltiptext: tooltiptext, localized: false, defaultArea: CustomizableUI.AREA_NAVBAR, onCreated(btn) { btn.style.setProperty("list-style-image", `url("${img}")`, "important"); if (favicons._favrunning) btn.style.setProperty("fill", "color-mix(in srgb, currentColor 20%, #e31b5d)"); }, onCommand(e) { favicons.favSearchStart(); }, }); })();
//Искать в............... (this.contextsearch = { topic: "browser-search-engine-modified", hide: "browser.search.hiddenOneOffs", defaultImg: "chrome://browser/skin/search-engine-placeholder.png", searchSelect: null, popup: null, init(that) { var searchSelect = this.searchSelect = document.querySelector("#context-searchselect"); if (!searchSelect) return; var popup = this.popup = searchSelect.closest("menupopup"); popup.addEventListener("popupshowing", this); that.unloadlisteners.push("contextsearch"); }, destructor() { this.popup.removeEventListener("popupshowing", this); if (this.popupshowing == this.handler) { this.popup.removeEventListener("popuphidden", this); Services.obs.removeObserver(this, this.topic); Services.prefs.removeObserver(this.hide, this); } }, handleEvent(e) { this[e.type](e); }, popupshowing(e) { var popup = this.popup; var searchSelect = this.searchSelect; if (e.target != popup || searchSelect.hidden) return; var menu = document.createXULElement("menu"); menu.className = "menu-iconic"; var menupopup = document.createXULElement("menupopup"); menu.append(menupopup); menu.ePopup = menupopup; searchSelect.collapsed = true; searchSelect.before(menu); menu.onclick = this.search.bind(this); this.handler = ev => { if (ev.target != popup) return; menu.hidden = searchSelect.hidden; }; this.handlerRebuild = () => this.handler(e) || this.rebuild(menu); this.popuphidden = ev => { if (ev.target != popup) return; menu.hidden = true; }; this.popup.addEventListener("popuphidden", this); this.rebuild(menu); }, getEngines() { var args = "hideOneOffButton" in Services.search.defaultEngine ? [e => !e.hideOneOffButton] : Object.defineProperty( [function(e) {return !this.includes(e.name);}], "1", { get: () => Services.prefs.getStringPref(this.hide)?.split(",") || [] } ); return (this.getEngines = async () => (await Services.search.getVisibleEngines()).filter(...args) )(); }, async rebuild(menu) { var de = Services.search.defaultEngine; de = de.wrappedJSObject || de; this.setAttrs(menu, de, `Искать в ${de.name} или в ...`); menu.ePopup.textContent = ""; for(let engine of await this.getEngines()) { if (engine == de) continue; var menuitem = document.createXULElement("menuitem"); menuitem.className = "menuitem-iconic"; this.setAttrs(menuitem, engine); menu.ePopup.append(menuitem); } this.popupshowing = this.handler; Services.obs.addObserver(this, this.topic, false); Services.prefs.addObserver(this.hide, this); }, setAttrs(node, engine, label = engine.name) { node.engine = engine; node.setAttribute("label", label); node.setAttribute("image", engine.iconURI ? engine.iconURI.spec : this.defaultImg); }, observe() { this.popupshowing = this.handlerRebuild; Services.obs.removeObserver(this, this.topic); Services.prefs.removeObserver(this.hide, this); }, search(e) { var {engine} = e.target; if (!engine) return; var searchSelect = this.searchSelect; var submission = engine.getSubmission( searchSelect.searchTerms, null, "contextmenu" ); if (submission) { let tab = gBrowser.addTab(submission.uri.spec, { postData: submission.postData, index: (gBrowser.selectedTab._tPos + 1), triggeringPrincipal: searchSelect.principal }); if (e.button == 0) gBrowser.selectedTab = tab; } var popup = this.popup; e.button != 1 && popup.state == "open" && popup.hidePopup(); } }).init(this);
Отредактировано egorsemenov06 (19-02-2024 15:56:33)
Отсутствует
Отсутствует
egorsemenov06
Пробуйте..
// ==UserScript== // @name Browser search engine // @author Vitaliy V. // @include main // @shutdown window.contextsearch.destructor(); // @note https://forum.mozilla-russia.org/viewtopic.php?pid=780283#p780283 // ==/UserScript== (this.contextsearch = { topic: "browser-search-engine-modified", hide: "browser.search.hiddenOneOffs", defaultImg: "chrome://browser/skin/search-engine-placeholder.png", searchSelect: null, popup: null, init(that) { var searchSelect = this.searchSelect = document.querySelector("#context-searchselect"); if (!searchSelect) return; var popup = this.popup = searchSelect.closest("menupopup"); popup.addEventListener("popupshowing", this); that.unloadlisteners?.push("contextsearch"); }, destructor() { this.popup.removeEventListener("popupshowing", this); if (this.popupshowing == this.handler) { this.popup.removeEventListener("popuphidden", this); Services.obs.removeObserver(this, this.topic); Services.prefs.removeObserver(this.hide, this); } }, handleEvent(e) { this[e.type](e); }, popupshowing(e) { var popup = this.popup; var searchSelect = this.searchSelect; if (e.target != popup || searchSelect.hidden) return; var menu = document.createXULElement("menu"); menu.className = "menu-iconic"; var menupopup = document.createXULElement("menupopup"); menu.append(menupopup); menu.ePopup = menupopup; searchSelect.style.setProperty("display", "none", "important"); searchSelect.before(menu); menu.onclick = this.search.bind(this); this.handler = e => e.target != popup || (menu.hidden = searchSelect.hidden); this.handlerRebuild = e => this.handler(e) || this.rebuild(menu); this.popuphidden = ev => { if (ev.target != popup) return; menu.hidden = true; }; this.popup.addEventListener("popuphidden", this); this.rebuild(menu); }, getEngines() { var args = "hideOneOffButton" in Services.search.defaultEngine ? [e => !e.hideOneOffButton] : Object.defineProperty( [function(e) {return !this.includes(e.name);}], "1", { get: () => Services.prefs.getStringPref(this.hide)?.split(",") || [] } ); return (this.getEngines = async () => (await Services.search.getVisibleEngines()).filter(...args) )(); }, async rebuild(menu) { var de = Services.search.defaultEngine; de = de.wrappedJSObject || de; this.setAttrs(menu, de, `Искать в ${de.name} или в ...`); menu.ePopup.textContent = ""; for(let engine of await this.getEngines()) { if (engine == de) continue; var menuitem = document.createXULElement("menuitem"); menuitem.className = "menuitem-iconic"; this.setAttrs(menuitem, engine); menu.ePopup.append(menuitem); } this.popupshowing = this.handler; Services.obs.addObserver(this, this.topic, false); Services.prefs.addObserver(this.hide, this); }, setAttrs(node, engine, label = engine.name) { node.engine = engine; node.setAttribute("label", label); node.setAttribute("image", engine._iconURI ? engine._iconURI.spec : engine.iconURI ? engine.iconURI.spec : this.defaultImg); }, observe() { this.popupshowing = this.handlerRebuild; Services.obs.removeObserver(this, this.topic); Services.prefs.removeObserver(this.hide, this); }, search(e) { var {engine} = e.target; if (!engine) return; var searchSelect = this.searchSelect; var submission = engine.getSubmission( searchSelect.searchTerms, null, "contextmenu" ); if (submission) { let tab = gBrowser.addTab(submission.uri.spec, { postData: submission.postData, index: (gBrowser.selectedTab._tPos + 1), triggeringPrincipal: searchSelect.principal }); if (e.button == 0) gBrowser.selectedTab = tab; } var popup = this.popup; e.button != 1 && popup.state == "open" && popup.hidePopup(); } }).init(this);
Жизнь иногда такое выкидывает, что хочется подобрать...
Отсутствует
egorsemenov06
Пробуйте..Browser search engineВыделить кодКод:
// ==UserScript== // @name Browser search engine // @author Vitaliy V. // @include main // @shutdown window.contextsearch.destructor(); // @note https://forum.mozilla-russia.org/viewtopic.php?pid=780283#p780283 // ==/UserScript== (this.contextsearch = { topic: "browser-search-engine-modified", hide: "browser.search.hiddenOneOffs", defaultImg: "chrome://browser/skin/search-engine-placeholder.png", searchSelect: null, popup: null, init(that) { var searchSelect = this.searchSelect = document.querySelector("#context-searchselect"); if (!searchSelect) return; var popup = this.popup = searchSelect.closest("menupopup"); popup.addEventListener("popupshowing", this); that.unloadlisteners?.push("contextsearch"); }, destructor() { this.popup.removeEventListener("popupshowing", this); if (this.popupshowing == this.handler) { this.popup.removeEventListener("popuphidden", this); Services.obs.removeObserver(this, this.topic); Services.prefs.removeObserver(this.hide, this); } }, handleEvent(e) { this[e.type](e); }, popupshowing(e) { var popup = this.popup; var searchSelect = this.searchSelect; if (e.target != popup || searchSelect.hidden) return; var menu = document.createXULElement("menu"); menu.className = "menu-iconic"; var menupopup = document.createXULElement("menupopup"); menu.append(menupopup); menu.ePopup = menupopup; searchSelect.style.setProperty("display", "none", "important"); searchSelect.before(menu); menu.onclick = this.search.bind(this); this.handler = e => e.target != popup || (menu.hidden = searchSelect.hidden); this.handlerRebuild = e => this.handler(e) || this.rebuild(menu); this.popuphidden = ev => { if (ev.target != popup) return; menu.hidden = true; }; this.popup.addEventListener("popuphidden", this); this.rebuild(menu); }, getEngines() { var args = "hideOneOffButton" in Services.search.defaultEngine ? [e => !e.hideOneOffButton] : Object.defineProperty( [function(e) {return !this.includes(e.name);}], "1", { get: () => Services.prefs.getStringPref(this.hide)?.split(",") || [] } ); return (this.getEngines = async () => (await Services.search.getVisibleEngines()).filter(...args) )(); }, async rebuild(menu) { var de = Services.search.defaultEngine; de = de.wrappedJSObject || de; this.setAttrs(menu, de, `Искать в ${de.name} или в ...`); menu.ePopup.textContent = ""; for(let engine of await this.getEngines()) { if (engine == de) continue; var menuitem = document.createXULElement("menuitem"); menuitem.className = "menuitem-iconic"; this.setAttrs(menuitem, engine); menu.ePopup.append(menuitem); } this.popupshowing = this.handler; Services.obs.addObserver(this, this.topic, false); Services.prefs.addObserver(this.hide, this); }, setAttrs(node, engine, label = engine.name) { node.engine = engine; node.setAttribute("label", label); node.setAttribute("image", engine._iconURI ? engine._iconURI.spec : engine.iconURI ? engine.iconURI.spec : this.defaultImg); }, observe() { this.popupshowing = this.handlerRebuild; Services.obs.removeObserver(this, this.topic); Services.prefs.removeObserver(this.hide, this); }, search(e) { var {engine} = e.target; if (!engine) return; var searchSelect = this.searchSelect; var submission = engine.getSubmission( searchSelect.searchTerms, null, "contextmenu" ); if (submission) { let tab = gBrowser.addTab(submission.uri.spec, { postData: submission.postData, index: (gBrowser.selectedTab._tPos + 1), triggeringPrincipal: searchSelect.principal }); if (e.button == 0) gBrowser.selectedTab = tab; } var popup = this.popup; e.button != 1 && popup.state == "open" && popup.hidePopup(); } }).init(this);
Большое Спасибо эта рабоьает как надо !!!!!А с первой не поможите?
Отсутствует
А с первой не поможите?
Это баг 1872673 - Remove 'console' export from Console.sys.mjs
То есть, дело не в коде кнопки, а в само́м UCF.
Но держать отладочный консольский стафф в кнопке постоянно
не требуется, можешь просто удалить console.log(req)
Однако, вернуть в укфский сандбокс консоль не помешает.
Я тут в user_chrome.js так переставлял
/* if ("defineLazyModuleGetters" in XPCOMUtils) XPCOMUtils.defineLazyModuleGetters(scope, { console: "resource://gre/modules/Console.jsm", AddonManager: "resource://gre/modules/AddonManager.jsm", AppConstants: "resource://gre/modules/AppConstants.jsm", E10SUtils: "resource://gre/modules/E10SUtils.jsm", FileUtils: "resource://gre/modules/FileUtils.jsm", OS: "resource://gre/modules/osfile.jsm", PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", setTimeout: "resource://gre/modules/Timer.jsm", setTimeoutWithTarget: "resource://gre/modules/Timer.jsm", clearTimeout: "resource://gre/modules/Timer.jsm", setInterval: "resource://gre/modules/Timer.jsm", setIntervalWithTarget: "resource://gre/modules/Timer.jsm", clearInterval: "resource://gre/modules/Timer.jsm", }); */ var data = { AddonManager: null, AppConstants: null, E10SUtils: null, FileUtils: null, PlacesUtils: null, Timer: ["setTimeout", "setTimeoutWithTarget", "clearTimeout", "setInterval", "setIntervalWithTarget", "clearInterval"] }; var sfx, def, modules = {}; var vers = parseInt(Services.appinfo.platformVersion); if (vers <= 114) data.osfile = "OS"; if (vers <= 122) def = XPCOMUtils.defineLazyModuleGetters, sfx = "jsm", data.Console = "console"; else def = ChromeUtils.defineESModuleGetters, sfx = "sys.mjs", ChromeUtils.defineLazyGetter(scope, "console", () => Cu.getGlobalForObject(Cu).console.createInstance()); var set = (key, val) => modules[key] = `resource://gre/modules/${val}.${sfx}`; for(var key in data) { var val = data[key] || key; if (Array.isArray(val)) for(var str of val) set(str, key); else set(val, key); } def(scope, modules);
Отсутствует
egorsemenov06 пишетА с первой не поможите?
Это баг 1872673 - Remove 'console' export from Console.sys.mjs
То есть, дело не в коде кнопки, а в само́м UCF.Но держать отладочный консольский стафф в кнопке постоянно
не требуется, можешь просто удалить console.log(req)Однако, вернуть в укфский сандбокс консоль не помешает.
Я тут в user_chrome.js так переставлялскрытый текстВыделить кодКод:
/* if ("defineLazyModuleGetters" in XPCOMUtils) XPCOMUtils.defineLazyModuleGetters(scope, { console: "resource://gre/modules/Console.jsm", AddonManager: "resource://gre/modules/AddonManager.jsm", AppConstants: "resource://gre/modules/AppConstants.jsm", E10SUtils: "resource://gre/modules/E10SUtils.jsm", FileUtils: "resource://gre/modules/FileUtils.jsm", OS: "resource://gre/modules/osfile.jsm", PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", setTimeout: "resource://gre/modules/Timer.jsm", setTimeoutWithTarget: "resource://gre/modules/Timer.jsm", clearTimeout: "resource://gre/modules/Timer.jsm", setInterval: "resource://gre/modules/Timer.jsm", setIntervalWithTarget: "resource://gre/modules/Timer.jsm", clearInterval: "resource://gre/modules/Timer.jsm", }); */ var data = { AddonManager: null, AppConstants: null, E10SUtils: null, FileUtils: null, PlacesUtils: null, Timer: ["setTimeout", "setTimeoutWithTarget", "clearTimeout", "setInterval", "setIntervalWithTarget", "clearInterval"] }; var sfx, def, modules = {}; var vers = parseInt(Services.appinfo.platformVersion); if (vers <= 114) data.osfile = "OS"; if (vers <= 122) def = XPCOMUtils.defineLazyModuleGetters, sfx = "jsm", data.Console = "console"; else def = ChromeUtils.defineESModuleGetters, sfx = "sys.mjs", ChromeUtils.defineLazyGetter(scope, "console", () => Cu.getGlobalForObject(Cu).console.createInstance()); var set = (key, val) => modules[key] = `resource://gre/modules/${val}.${sfx}`; for(var key in data) { var val = data[key] || key; if (Array.isArray(val)) for(var str of val) set(str, key); else set(val, key); } def(scope, modules);
Большое Спасибо!!!!теперь отлично
Отсутствует
В шапке бы поправить ссылку на рабочую - "Восстановить фавиконки закладок".
Похож чем-то, с тем что выше. Тоже рабочий, пользуюсь. ucf_contextsearch.js
Да они идентичны, с мелкими различиями в коде)
Отредактировано b0ttle (19-02-2024 21:44:24)
Отсутствует
В шапке бы поправить ссылку
На этом форуме нет шапок, есть только первый пост, посему это могут сделать либо создатель либо модератор...
Да они идентичны, с мелкими различиями в коде)
Если у вас все работает,
Тоже рабочий, пользуюсь. ucf_contextsearch.js
пользуйтесь... Речь шла про 1945 год, ой про 123 найди различия...
Отредактировано Farby (19-02-2024 22:22:14)
Жизнь иногда такое выкидывает, что хочется подобрать...
Отсутствует
Farby
Сорри, этот момент упустил. Вы его сами подправили, вроде разбирайтесь в коде?
Надеюсь, что обновят некоторые ссылки с первого поста, а так скорее затеряется.
Отредактировано b0ttle (19-02-2024 23:27:47)
Отсутствует
В шапке бы поправить ссылку на рабочую - "Восстановить фавиконки закладок".
Раз это не в скрипте проблема, шапку пока не менял.
Это баг 1872673 - Remove 'console' export from Console.sys.mjs
То есть, дело не в коде кнопки, а в само́м UCF.
Dumby - Благодарю!
Обновил на гитхабе chrome/user_chrome_files/user_chrome.js
в Demo-профиле - сборке полезных скриптов немного поправил:
user_chrome.js, UcfPrefs.jsm, ucf_UrlTooltip, ucf_contextmenuopenwith, ucf_contextsearch, ucf_hookClicks
Отсутствует
Раз это не в скрипте проблема
Кстати, скрипт под багом ходит (1552815).
Движуха там затихла, но в любой момент это дело могут пнуть,
и баг вывалится как "FIXED". Прикинь тогда разгребать.
.jsm
А вот и баг о временах и сроках.
Может завести какую-нибудь папку с любым условным названием типа «129».
Запилить на неё readme, мол здесь вам (пока) не что-то готовое,
но всего лишь WIP-полигон миграции JSM —> ESM.
Это я в том смысле, что время пока есть,
и «подстелить соломку», а не чтобы как «снег на голову».
Совместимость, разумеется, может и должна быть сброшена.
Отсутствует