aminomancer / uc.css.js

A dark indigo CSS theme for Firefox and a large collection of privileged scripts to add new buttons, menus, and behaviors and eliminate nuisances. The theme is similar to other userChrome stylesheets, but it's intended for use with an autoconfig loader like fx-autoconfig, since it uses JavaScript to implement its more functional features.
Other
329 stars 27 forks source link

customHintProvider.uc.js #26

Closed Mitezuss closed 2 years ago

Mitezuss commented 2 years ago

Is your feature request related to a problem? Please describe. No, just the Timeout

Describe the solution you'd like Infinite timeout

Just add a condictional to:

this._timerID = setTimeout(() => {
this._panel.hidePopup(true);
this._animationBox.removeAttribute("hidden");
}, DURATION + 120);

Like if (DURATION == -1) (or whatever) then not autoclose (the popup will close when press click)

Regards

aminomancer commented 2 years ago

Okay, try version 1.1.2. But FYI I think this might be a red herring. If you're feeling the need to make the popup stick around until user interaction, that's a pretty good signal that you don't want a confirmation hint at all, but rather some other kind of popup. Since the whole point of a confirmation hint is just to show the user that something happened, not to require further action, it sounds like what you're looking for is a popupnotification or gNotificationBox.appendNotification.

The custom hint is really just a modified alias of the confirmation hint, so it doesn't have any buttons either. It's not immediately obvious that you need to do something to get rid of it, and the only things you can do to manually get rid of it are click something else in the browser chrome, or press the Escape key. So it's not exactly intuitive. If the popup stays open indefinitely, the user might perceive it as a bug. Conversely, Firefox has some other components that do expect interaction and are designed to stick around permanently, like <notification> and <popupnotification>.

So, if I was trying to make a non-transient hit that requires interaction, I'd use one of those, or I'd use the AppMenuNotifications system to make it integrate neatly with the most common built-in type of sticky notification.

Mitezuss commented 2 years ago

@aminomancer If you're feeling the need to make the popup stick around until user interaction

Yes, i wanna do a popup (not notification) to show some text when a user do a click (and close when press accept or click on the page)

Because i don't know how to do (i know it is possible to do with a xul file, but wanna on pure javascript), so maybe i will use the "hint" function.

Regards

aminomancer commented 2 years ago

How were you doing it with a xul file? It should be easy to convert to an autoconfig script

Mitezuss commented 2 years ago

@aminomancer

Done with AppMenuNotifications system.

With extras modifications (for set size, font, buttons, position, text, icon, etc...) (dynamically)

Thanks for the info =)

aminomancer commented 2 years ago

Oh, I don't mean how were you doing it conceptually. I mean send me the actual xul file so I can show you how to turn it into an autoconfig script.

Mitezuss commented 2 years ago

For example i want to do some like:

<tooltip id="ResultTip" style="background-color:InfoBackground;color:InfoText;border:1px solid InfoText;width:450px;">
    <vbox id="ResultPopupBox">
        <label id="ResultLabel" flex="1" onclick="CopyToClipboard(event,this.textContent);" tooltiptext="Click to Copy"/>
    </vbox>
</tooltip>

Regards

PD: anyway i did with the AppMenuNotifications system, work fine and nice, but is good learn :smiley:

aminomancer commented 2 years ago

I don't think that would work even if Firefox still supported XUL addons. Tooltips can't be clicked, by definition. That's the kind of thing you want a element for. See this script for example. But ignoring the fact that tooltips aren't clickable, you could easily create that in a script. You just use

let tooltip = MozXULElement.parseXULToFragment(
    `<tooltip id="ResultTip" style="background-color:InfoBackground;color:InfoText;border:1px solid InfoText;width:450px;">
        <vbox id="ResultPopupBox">
            <label id="ResultLabel" flex="1" onclick="CopyToClipboard(event,this.textContent);" tooltiptext="Click to Copy"/>
        </vbox>
    </tooltip>`
);
document.getElementById("mainPopupSet").appendChild(tooltip);
document.getElementById("ResultTip").openPopup(anchorElement);
Mitezuss commented 2 years ago

@aminomancer

Hi, i have a "conseptual" question, i am triying to avoid to use addEventListener.

I know the lister is unique only when the function was declared (so, the function is not anonymous).

But, for example i have this action:

let MenuArea = document.getElementById('contentAreaContextMenu');
MenuArea.addEventListener("popupshowing", ()=>{ ItemSeeker.hidden = !(gContextMenu.isTextSelected); }, false);

How you see, the item will be showed only when there are text selected

But, i am thinking: that listener when is add ? Just one time after start the window of firefox, but when launch another instance, of firefox, the script will start again and add the item and the listener. So, the will exist 2 listeners ? and they will shutdown when the window be closed ?

So, my question is about:

Is that fine ? One listener will die when a window is closed? Are there a better option ? for show and hide a item (without use addEventListener("popupshowing"...)

Regards

PD: Sorry my english is bad =C and it is a offtopic xD

aminomancer commented 2 years ago

You're using fx-autoconfig right? If you use fx-autoconfig the script will run once for every window. The script will run inside the execution context of the window, so script instance 1 (running inside window 1) couldn't add an event listener to window 2's context menu. Each window is basically its own execution context. Technically you can see other windows from js but you'd need to use XPCOM methods to do that, like Services.ww or Services.wm.

Anyway, that's where it runs. As for when it runs, it happens as soon as the window is created. That's why I usually try to slow it down by using this:

function init() {
    let MenuArea = document.getElementById("contentAreaContextMenu");
    MenuArea.addEventListener(
        "popupshowing",
        () => {
            ItemSeeker.hidden = !gContextMenu.isTextSelected;
        },
        false
    );
}
if (gBrowserInit.delayedStartupFinished) init();
else {
    let delayedListener = (subject, topic) => {
        if (topic == "browser-delayed-startup-finished" && subject == window) {
            Services.obs.removeObserver(delayedListener, topic);
            init();
        }
    };
    Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished");
}

Yes, there will be 2 separate event listeners, each running in their own window. All event listeners on DOM nodes are removed when a window is closed, since the entire window is collapsed and wiped except for (sometimes) something called a xul store. When a window is deleted from memory so are all the event listeners instantiated that target it. The only reason some scripts provide "destroy" functions is because they're trying to support a feature where you can disable the script without quitting firefox. So to disable the script during runtime it needs to be able to clean itself up. But that's not necessary (or possible) with fx-autoconfig. There are other types of listeners you would need to close up yourself though. Services.obs.addObserver adds notification observers that are above the level of the window, they won't be removed when a window closes. Same with CustomizableUI.addListener. But you're not using any of that here.

Adding items to the content area context menu is especially difficult, because unlike other context menus, it is built with a JSActor. So all its code is out of reach. All you have to mess with it is the DOM and gContextMenu (which only exists when the menu's showing). It's possible to do this other ways but really, from autoconfig, your best bet is to do what you did. See here for a larger example. At the end of the day, it's listening to popupshowing and popuphidden and hiding the item accordingly.

The only other approaches are to modify ContextMenuChild.jsm and ContextMenuParent.jsm which is really not feasible, or to use a webextension instead. Webextensions have an easy API for adding context menu items but there are many downsides. They don't feel native, they're slower to hide/show, etc. So basically what you're doing here is the right approach.

Mitezuss commented 2 years ago

Thank for your explain, it was profit.

...gContextMenu (which only exists when the menu's showing). It's possible to do this other ways... ...The only other approaches are to modify ContextMenuChild.jsm and ContextMenuParent.jsm which is really not feasible...

So, the only form to import/invoque some function inside of gContextMenu (without open the context) is modding that .jsm ? or are there another form to import magically (or some ugly workaround) some function ?

...Services.obs.addObserveradds notification observers...

I am using:

var AppMenuObserver = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        //Code to add item on appmenu
    });
    if (!!document.querySelector(ItemID)) {
        AppMenuObserver.disconnect();
        //AppMenuObserver = null; //**Is it okey, to do ?**
    }
});
AppMenuObserver.observe(document.querySelector("#PanelUI-menu-button"), { attributes: true, attributeFilter: ['open'] });

For add item to AppMenu (setting the position), but i see your code (to add restar, for example):

https://github.com/aminomancer/uc.css.js/blob/9c2b9e8f6e1b5ddc040c09d8ed4a726deeb74d16/JS/appMenuMods.uc.js#L51

So: i will try your code. I did with your code too

But, have a question about MutationObserver: only can be disconnect ? (maybe can do "AppMenuObserver=null" but is a workaround too forced) or can be deleted/destroy ? From (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe) "disconnect" mean "he keep existing" right ? I would like to remove it, like another observers.

Edit: on your script: https://github.com/aminomancer/uc.css.js/blob/9c2b9e8f6e1b5ddc040c09d8ed4a726deeb74d16/JS/urlbarNotificationIconsOpenStatus.uc.js#L31

So, i think is fine, set to null; and that is all

aminomancer commented 2 years ago

MutationObserver is a horrible way to do this. There's no reason to do that. You're listening for the open attribute, but the open attribute is only added after openPopup executes. So you can just listen for popupshowing and your callback will happen sooner and it will have much less of a performance impact. MutationObserver should only be used when absolutely necessary because it's supposedly about 7 times worse than addEventListener in terms of performance.

And sort of, calling disconnect stops the observing but it keeps the MutationObserver instance in memory. You created an object when you called var observer = new MutationObserver(...) so even after calling disconnect, the variable still points to that object in memory, but it's a very small amount of memory, not impacting performance at all. But yes making it null will destroy the reference to that object so the garbage collector will eventually clear the MutationObserver instance from memory. Not that it matters since it's just memory, it doesn't represent an ongoing impact on the event loop or CPU cycles or whatever. But again, there's absolutely no reason to use MutationObserver for this in the first place. This way is much better:

function init() {
    PanelUI._initialized || PanelUI.init(shouldSuppressPopupNotifications);
    document.getElementById("appMenu-popup").addEventListener(
        "popupshowing",
        (e) => {
            //Code to add item on appmenu
        },
        { once: true }
    );
}
if (gBrowserInit.delayedStartupFinished) init();
else {
    let delayedListener = (subject, topic) => {
        if (topic == "browser-delayed-startup-finished" && subject == window) {
            Services.obs.removeObserver(delayedListener, topic);
            init();
        }
    };
    Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished");
}

The { once: true } parameter will remove the listener after its first trigger. So that does what you want. The script you linked, urlbarNotificationIconsOpenStatus.uc.js is a pretty old script. It's not the optimal way of doing that. I will update it later since I'm certain there's a better way.

As for your first question, MutationObserver has nothing to do with Services.obs.addObserver. MutationObserver is something implemented in all js contexts, something used for web development too. Services.obs is the Notification Observer Service. It's only implemented in Firefox's privileged code, so a web developer wouldn't use it, it's not part of any web spec. It's just something internal to Firefox but something you can use in autoconfig scripts. It's similar to what an extension might do with sendAsyncMessage, if you're familiar with webextensions.

Basically I can dispatch a notification like Services.obs.notifyObservers(subject, topic, data) where topic is some string like "button-clicked" or "user-logged-in" and subject and data can be anything. For subject you might pass the subject of the notification so it's kind of like event.target. Data is usually left blank but sometimes you might send an object with multiple properties if you're doing something complicated. And that notification I dispatched will do nothing unless something is listening for it. So I'd need to also call Services.obs.addObserver(function(subject, topic, data), topic) to add an observer (a listener). Then when notifyObservers is called, if its topic is the same as the one I passed to addObserver in argument 2, it dispatches the message to my observer, which then calls the callback I passed as argument 1.

The reason to use this is that it works across execution contexts. When I call addObserver it will "hear" notifications no matter where in firefox they came from. They could come from another window, from a sandboxed browser, etc., doesn't matter. I'll hear about them just the same. That's why subject exists, since my callback probably needs to know where the notification came from to discern if it's relevant to this particular instance of the observer. Like if I'm listening to a hypothetical topic called "button-clicked" then I wanna make sure the button that was clicked is in the same window my callback is for.

aminomancer commented 2 years ago

Actually an even safer way to do this is with the ViewShowing event since it ensures that the correct view is showing. Since the app menu has multiple views, popupshowing may not handle it. So you should just do it the way I did in appMenuMods.uc.js:

function init() {
    PanelUI._initialized || PanelUI.init(shouldSuppressPopupNotifications);
    PanelUI.mainView.addEventListener(
        "ViewShowing",
        (e) => {
            //Code to add item on appmenu
        },
        { once: true }
    );
}
if (gBrowserInit.delayedStartupFinished) init();
else {
    let delayedListener = (subject, topic) => {
        if (topic == "browser-delayed-startup-finished" && subject == window) {
            Services.obs.removeObserver(delayedListener, topic);
            init();
        }
    };
    Services.obs.addObserver(delayedListener, "browser-delayed-startup-finished");
}

If it's not the mainview you're after (maybe you care about the bookmarks subview or the help subview or something) then you wanna add the listener to those instead of PanelUI.mainView. So you'd do that like this:

PanelMultiView.getViewNode(document, "PanelUI-bookmarks").addEventListener("ViewShowing", ...)
Mitezuss commented 2 years ago

because it's supposedly about 7 times worse than addEventListener

Yes, i was thinking about that, so for that my question.

The { once: true } parameter will remove the listener

Yes, i am using { once: true } (or deleting when finish the task) and to avoid to use listener i am using setAttribute (i think is better)

Services.obs.notifyObservers(subject, topic, data)

Thank for the explain, was good.

Actually an even safer way to do this is with the ViewShowing...

Yes, i am using that code.

Regards

aminomancer commented 2 years ago

No, don't use setAttribute to add listeners. There can only be one attribute listener for each event type, you don't want to interfere with internal code. It won't work with ViewShowing anyway, there is no "onviewshowing" attribute etc. Those only exist for the DOM spec events

Mitezuss commented 2 years ago

No, don't use setAttribute to add listeners.

Maybe I do not explain it well. I use setAttribute for oncommand, onclick, or whatever. Maybe, on some cases, can do a listener (like a script to undo tabs with middle click) but can be done setting a attribute on the tabbrowser-arrowscrollbox, to avoid use a lister to "click".

Maybe my english is bad, so maybe you understand better with a code example:

With setAttribute:

TabsBar = document.getElementById("tabbrowser-arrowscrollbox");
TabsBar.setAttribute('onclick', "MidUndo.UndoTab(event)");

With listener:

gBrowser.tabContainer.addEventListener('click', UndoTab, true);

What you think about that ? (about efficient and performance) I know maybe the code for the lister is more portable, but the workaround to use setAttribute is better than listen all time to click

.

By the way, about: sendAsyncMessage

the only form now to link content and chrome is the message, or are there another form ? (I'm not sure, but in the first versions of Firefox, the contect could be accessed directly)

Regards

aminomancer commented 2 years ago

Mitezuss, you don't seem to understand. Like I said before, there is no efficiency or performance difference between these two code examples. They are both doing the same thing. onclick is just an attribute that Firefox (and other browsers) pays attention to, and when it gets added, Firefox turns its string value "MidUndo.UndoTab(event)" into an anonymous function expression and adds it as an event listener for the "click" event, i.e. function (event) { MidUndo.UndoTab(event) }

You can easily check and confirm this in the inspector. After you add a valid onclick attribute to an element, a gray box will appear on the right side of the element's row in the inspector. a gray box saying "event" and if you click that gray box you can see the listener and expand it to view the callback.

You could have discovered that very easily if you just looked up onclick on MDN.

Of course you can't access the content directly anymore, it's been isolated from the chrome for years. I already answered this question above at great length, see my remarks about JSActors and frame scripts.

Mitezuss commented 2 years ago

Now i understand.

Yes, i saw that event on the inspector. So, my fauld.

So, the best option is set a listener, right ? (with the function NOT annonymous)

Thanks

aminomancer commented 2 years ago

Doesn't really matter whether the function is anonymous or not since it's extremely tiny, it just depends on whether you need to reference the callback elsewhere.

Mitezuss commented 2 years ago

@aminomancer

I made some modifications to "Paste and Go" (with some workaround), so i wanna ask you if is factible to remove this event:

0

Are there some form to remove "natives event" ?

My little modification do:

left : native pastego mid: paste and go tab right: paste and go tab on background

But, for that, i do a workaround to avoid the native event (re-setting the command)

And, if you have time, check this:

https://github.com/ardiman/userChrome.js/blob/master/alwaysontop/AlwaysOnTop.uc.xul

I am trying to implement this (to set ontop the consola and toolbox, not the firefox window) but i am not sure if (on current firefox verison) can handle some like (for win7)

Components.utils.import("resource://gre/modules/ctypes.jsm");
var lib = ctypes.open("user32.dll");
aminomancer commented 2 years ago

The event is anonymous so removing it is impossible. Event handlers can block downstream events with event.stopImmediatePropagation but you would need to make sure your event handler is added before the built-in one, since they're executed in order of creation. In this case it's not easy to prevent the event handler from being created, either. Normally I'd suggest to create a new menuitem and remove the built-in one, but the way Firefox creates it is pretty messy. It adds an anonymous event handler to the menupopup itself which references the original menuitem. If you remove the built-in one, that reference will break and there'll be no way to fix it. So yeah it's just quite impractical to change this at the command level. Instead you should probably just add a mouseup listener since 1) only mouse events will have left/mid/right buttons anyway, and 2) mouseup events are upstream of click and command events, so you can interrupt the process there like this:

let pasteAndGoListener = function(event) {
    switch (event.button) {
        case 0:
            // propagate to built-in handler
            return;
        case 1:
            // paste and go tab...
            break;
        case 2:
            // paste and go background...
            break;
    }
    event.preventDefault();
    event.stopImmediatePropagation();
    event.stopPropagation();
}

document.getElementById("paste-and-go").addEventListener("onmouseup", pasteAndGoListener);

As for your other question, like I have already said, you can check this stuff yourself with searchfox.org. I don't have time to keep answering every minute question you have, you can easily search for this stuff yourself. If you just searched ctypes.jsm you'd see this still exists. I don't know if it works, I don't use dynamic link libraries. Anyway, you should use ChromeUtils.import, not Components.utils.import.