How to use a main loop in a Gnome Extension? - loops

I want to display the content of a file in the Gnome top bar and update the display when the content of the file changes.
For now I have just a skeleton extension that writes Hello world in the top bar.
I have tried to make a loop that should update the text:
File: extension.js
const {St, Clutter} = imports.gi;
const Main = imports.ui.main;
let panelButton;
function init () {
// Create a Button with "Hello World" text
panelButton = new St.Bin({
style_class : "panel-button",
});
let panelButtonText = new St.Label({
text : "Hello World",
y_align: Clutter.ActorAlign.CENTER,
});
panelButton.set_child(panelButtonText);
let i = "x"
const Mainloop = imports.mainloop;
let timeout = Mainloop.timeout_add_seconds(2.5, () => {
i = i + "x"
panelButtonText.set_text(i)
});
}
function enable () {
// Add the button to the panel
Main.panel._rightBox.insert_child_at_index(panelButton, 0);
}
function disable () {
// Remove the added button from panel
Main.panel._rightBox.remove_child(panelButton);
}
I expect the Hello world text to change multiple times but it stops at xx:
I have tried to do the same with date and time but it does not work either:
const GLib = imports.gi.GLib;
let now = GLib.DateTime.new_now_local();
let nowString = now.format("%Y-%m-%d %H:%M:%S");
panelButtonText.set_text(nowString)
Date and time does not update!

You'll need to return GLib.SOURCE_CONTINUE (true) for the event to keep looping, or GLib.SOURCE_REMOVE (false) for it to exit. Because you are not returning a value from your callback, it is being coerced from undefined to false and only run once.
More notes:
you will want to use GLib's functions now, not the MainLoop import, which is deprecated
you will want to store the returned GLib.Source ID (i.e. timeout), probably in the same scope as panelButton, so that you can remove it in disable().
There is a guide for using the mainloop on gjs.guide at https://gjs.guide/guides/gjs/asynchronous-programming.html#the-main-loop

Related

Command handling in Qooxdoo multi window application

I want to create a Qooxdoo application that consists of a Desktop with multiple Windows. The Desktop (and not each Window) also has a (common) ToolBar.
Now I want to have a command to "save" the document of the active window. This command can be triggered by the keyboard shortcut "Ctrl+S" as well as a button on the toolbar.
To handle the "Ctrl+S" to reach the currently active window the qx.ui.command.GroupManager (as described by https://qooxdoo.org/documentation/v7.5/#/desktop/gui/interaction?id=working-with-commands and https://qooxdoo.org/qxl.demobrowser/#ui~CommandGroupManager.html) is supposed to be the best solution. And I could easily implement that in my code.
But now I'm struggling to make the toolbar save button also call the save command of the currently active window as I don't know how to bind it correctly to the GroupManager.
An example code to get started in the playground https://qooxdoo.org/qxl.playground/:
// NOTE: run this script only once. Before running it again you need to reload
// the page as it seems that the commands are accumulation and not reset.
// I guess that this is a bug in the Playground
const root = this.getRoot();
qx.Class.define('test.Application',
{
extend : qx.application.Standalone,
members: {
main: function() {
const layout = new qx.ui.layout.VBox(5);
const container = new qx.ui.container.Composite(layout);
root.add(container, {edge: 0});
const windowManager = new qx.ui.window.Manager();
const desktop = new qx.ui.window.Desktop(windowManager);
this._manager = new qx.ui.command.GroupManager();
const menuBar = new qx.ui.menubar.MenuBar();
let menu = new qx.ui.menu.Menu();
///////////////////////////
// TODO: Call _doSave() of the active window!
let saveMenuButton = new qx.ui.menu.Button('Save','#MaterialIcons/save/16');
///////////////////////////
menu.add(saveMenuButton);
var fileMenu = new qx.ui.menubar.Button('File', null, menu);
menuBar.add(fileMenu);
const toolBar = new qx.ui.toolbar.ToolBar();
///////////////////////////
// TODO: Call _doSave() of the active window!
let saveToolBarButton = new qx.ui.toolbar.Button('Save','#MaterialIcons/save/16');
///////////////////////////
toolBar.add(saveToolBarButton);
container.add(menuBar,{flex:0});
container.add(toolBar,{flex:0});
container.add(desktop,{flex:1});
this._foo1 = new test.Window('foo1', this);
desktop.add(this._foo1);
this._foo1.open();
this._foo1.moveTo(100,20);
this._foo2 = new test.Window('foo2', this);
desktop.add(this._foo2);
this._foo2.open();
this._foo2.moveTo(200,100);
this._foo3 = new test.Window('foo3', this);
desktop.add(this._foo3);
this._foo3.open();
this._foo3.moveTo(300,180);
},
getGroupManager() {
return this._manager;
}
}
});
qx.Class.define('test.Window', {
extend: qx.ui.window.Window,
construct(windowName, controller) {
this.base(arguments, windowName);
this._name = windowName;
let commandGroup = new qx.ui.command.Group();
const cmd = new qx.ui.command.Command("Ctrl+S");
cmd.addListener('execute', this._doSave, this);
commandGroup.add('save', cmd);
controller.getGroupManager().add(commandGroup);
this.addListener('changeActive', () => {
if (this.isActive()) {
controller.getGroupManager().setActive(commandGroup);
}
}, this);
},
members: {
_doSave() {
alert("save " + this._name);
}
}
});
a = new test.Application();
How should the saveMenuButton.setCommand() and saveToolBarButton.setCommand() should look like to always call the command of the active window?
You can control a current active window via Desktop class:
let saveToolBarButton = new qx.ui.toolbar.Button('Save');
saveToolBarButton.addListener("click", function(){
desktop.getActiveWindow()._doSave();
}, this);
Would be great for your solution imo is to create a separate command and add this command to buttons:
const saveActiveWindowCommand = new qx.ui.command.Command();
saveActiveWindowCommand.addListener("execute", function(){
desktop.getActiveWindow()._doSave();
}, this);
let saveMenuButton = new qx.ui.menu.Button('Save');
saveMenuButton.setCommand(saveActiveWindowCommand);
let saveToolBarButton = new qx.ui.toolbar.Button('Save');
saveToolBarButton.setCommand(saveActiveWindowCommand);
EDIT:
You could set commands dynamically for "Main Panel" menu buttons. Because there is only one instance of command pressing "Ctrl+S" will trigger only one command but maybe you would like that main bar save buttons have extra logic.
You have in application class next method which will be called from window class when changeActive event happens.
setSaveCommand(command){
this.saveMenuButton.setCommand(command);
this.saveToolBarButton.setCommand(command);
},
and in your Window class:
if (this.isActive()) {
controller.setSaveCommand(cmd);
controller.getGroupManager().setActive(commandGroup);
}

Is there a better approach to implementing Google Sheet's like cell functionality?

I'm building out a system in React that has tabular data with cells. Those cells are editable via contentEditable divs. It's functionally similar to google sheets. I'm working on the functionality where single click on the cell allows the user to override the current value of the cell and double clicking allows them to edit the value.
The functionality involved is basically this:
When single click on cell override the current value. (No cursor visible?)
When double click on cell allow the user to edit the current value. (Cursor visible, can move left and right of chars with arrowKeys)
When double clicked into the cell reformat value (removes trailing zero's for cents: 8.50 becomes 8.5)
When double clicked start the caret position at the end of the input.
When user clicks out of the cells reformat the current value to its appropriate format (example is a price cell)
My cell component looks like this:
(Note* useDoubleClick() is a custom hook I wrote that works perfectly fine and will call single/double click action accordingly)
export default function Cell({ name, value, updateItem }) {
const [value, setValue] = useState(props.value), // This stays uncontrolled to prevent the caret jumps with content editable.
[isInputMode, setIsInputMode] = useState(false),
cellRef = useRef(null);
// Handle single click. Basically does nothing right now.
const singleClickAction = () => {
if(isInputMode)
return;
}
// Handle double click.
const doubleClickAction = () => {
// If already input mode, do nothing.
if(isInputMode) {
return;
}
setIsInputMode(true);
setCaretPosition(); // Crashing page sometimes [see error below]
reformatValue();
}
// It's now input mode, set the caret position to the length of the cell's innerText.
const setCaretPosition = () => {
var range = document.createRange(),
select = window.getSelection();
range.setStart(cellRef.current.childNodes[0], cellRef.current.innerText.length);
range.collapse(true);
selectObject.removeAllRanges();
selectObject.addRange(range);
}
// Reformat innerText value to remove trailing zero's from cents.
const reformatValue = () => {
var updatedValue = parseFloat(value);
setValue(updatedValue);
}
const onClick = useDoubleClick(singleClickAction, doubleClickAction);
/*
* Handle input change. Pass innerText value to global update function.
* Because we are using contentEditable and render "" if !isInputMode
* we have override functionality.
*/
const onInput = (e) => {
props.updateItem(props.name, e.target.innerText);
}
// When cell is blurred, reset isInputMode
const onBlur = () => {
setIsInputMode(false);
cellRef.current.innerText = ""; // Without this single click will not override again after blur.
}
return (
<div
data-placeholder={value} // to view current value while isInputMode is false
class="cell-input"
contentEditable="true"
onClick={onClick}
onInput={onInput}
onBlur={onBlur}
ref={cellRef}
>
{isInputMode ? value : ""}
</div>
)
}
And here is some css so that the user can see the current value while isInputMode is false:
.cell-input {
:empty:before {
content: attr(data-placeholder);
}
:empty:focus:before {
content: attr(data-placeholder);
}
}
Now here are the issues I'm running into.
When I call the setCaretPosition function, there are no childNodes because I'm rendering the empty value ("") and crashes the page sometimes with the error- TypeError: Argument 1 ('node') to Range.setStart must be an instance of Node.
I have a $ inside cells that contain a price and I was setting that in the css with ::before and content: '$', but now I can't because of the data-placeholder snippet.
When double clicking into cell the cursor is not visible at all. If you click the arrows to move between characters it then becomes visible.
This solution has me pretty close to my desired output so I feel pretty good about it, but I think there might be a better way to go about it or a few tweaks within my solution that will be a general improvement. Would love to hear some ideas.

state variable resetting itself for no re

I have a React Component that includes a list of items. When the user double clicks an item, a new browser window opens that allows the user to edit the item. The opener (the component that opens the new browser window) needs to keep track of all the browser windows that it has opened and when they close. This is needed so that if the user double clicks an item that has already been opened, I can give focus to the existing window for that item rather than opening a new one. I'm trying to keep track of the open windows in a state variable. Here is the relevant code:-
const handleSalesItemDoubleClick = (salesItem) => {
var windowRef = window.open(process.env.REACT_APP_APP_SERVER + '/' + site.SiteCode + '/SalesItem/' + salesItem,
salesItem,
'width=1000,left=0,top=0,height=600,toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,directories=no,status=no'
)
windowRef.onload = function () {
windowRef.onbeforeunload = () => { handleWindowClose(salesItem) }
}
var tabs = [...openSalesItems, {salesItem: salesItem, window: windowRef}];
setOpenSalesItems(tabs);
}
const handleWindowClose = (salesItemId) => {
var tabs = openSalesItems.filter(function (tab) { return tab.salesItem !== salesItemId })
setOpenSalesItems(tabs);
}
setOpenSalesItems is the function that sets the state variable "openSalesItems".
When I open multiple new windows then close them in reverse order, the openSalesItems contains the correct array at all times but if I open multiple windows then close the window that I opened first, the openSalesItems array becomes empty.

React CSS animations that happen one after another using hooks

I am trying to make something where I have 8 images, with 4 shown at a time. I need them to have an animation when they enter and an animation when they exit. The first 4 exit when one of them is clicked. But I only see the enter animation. I assume because the enter and exit animation happen at the same time.
Is there a way to add a delay or something similar using hooks to make one happen before the other that is compatible with internet explorer (super important)? Code shown below:
const [question, setQuestion] = React.useState(props.Questions[0]);
const [animate, setAnimate] = React.useState("enter");
React.useEffect(() => {
if (Array.isArray(props.Questions) && props.Questions.length) {
// switch to the next iteration by passing the next data to the <Question component
setAnimate("enter");
setQuestion(props.Questions[0]);
} else {
// if there are no more iterations (this looks dumb but needs to be here and works)
$("#mrForm").submit();
}
});
const onStubClick = (e, QuestionCode, StubName) => {
e.preventDefault();
// store selected stub to be submitted later
var newResponse = response.concat({ QuestionCode, StubName });
setResponse(newResponse);
setAnimate("exit");
// remove the first iteration of stubs that were already shown
props.Questions.splice(0, 1);
if (props.QuestionText.QuestionTextLabel.length > 1) {
// remove first iteration of questiontext if applicable
props.QuestionText.QuestionTextLabel.splice(0, 1);
props.QuestionText.QuestionTextImage.splice(0, 1);
// switch the question text
setQuestionLabel({ Label: props.QuestionText.QuestionTextLabel[0], Image: props.QuestionText.QuestionTextImage[0] });
}
};

Detect react event from Tampermonkey

I'm enhancing a React front end with Tampermonkey , by adding highlights to show cursor location in a grid, and allowing users to directly enter data , rather than then enter data.
After 2 or 3 cursor moves or data entry the grid refreshes or updates - no page change - and looses the highlighting I set up.
I'd like to catch the refresh/update and reset the highlighting.
I'm a noob..
The network tab shows post events so I tried https://jsbin.com/dixelocazo/edit?js,console
var open = window.XMLHttpRequest.prototype.open,
send = window.XMLHttpRequest.prototype.send;
to try and use POST events to detect the refresh. No joy !
I also looked at ajax events.
No luck :(
Can someone point me in the right direction here ?
Once I catch the event, I can then reset the highlighting to fix the problem
Since normally the userscripts run in a sandbox, JavaScript functions or objects cannot be used directly by default, here's what you can do:
Disable the sandbox:
// #grant none
You won't be able to use any GM functions, though.
Run in the page context via unsafeWindow:
const __send = unsafeWindow.XMLHttpRequest.prototype.send;
unsafeWindow.XMLHttpRequest.prototype.send = function () {
this.addEventListener('loadend', e => {
console.log('intercepted', e);
}, {once: true});
__send.apply(this, arguments);
};
Use MutationObserver to detect changes in page DOM:
const observer = new MutationObserver(mutations => {
const matched = [];
for (const {addedNodes} of mutations) {
for (const n of addedNodes) {
if (!n.tagName)
continue;
if (n.matches('.prey:not(.my-highlight)')) {
matched.push(n);
} else if (n.firstElementChild) {
matched.push(...n.querySelectorAll('.prey:not(.my-highlight)'));
}
}
}
// process the matched elements
for (const el of matched) {
el.classList.add('my-highlight');
}
});
observer.observe(document.querySelector('.surviving-ancestor') || document.body, {
subtree: true,
childList: true,
});
.surviving-ancestor means the element that isn't replaced/recreated by the page script. In devtools element inspector it's the one that isn't highlighted temporarily during DOM updates.
See also Performance of MutationObserver.

Resources