Complex key bindings in Gtk - gtk3

Some text editors like sublime or atom support complex key bindins, when you press and release two or more buttons while holding Ctrl or any other modifier key depressed to trigger some action.
There seems to be no direct support of such a concept in Gtk. Of course, one can do something like this:
static second_event_happened = FALSE;
ctrl_k_cb () {
g_timeout_add (500, ctrl_k_timeout, NULL) // queue watching function
// adjust accels so that until timeout ctrl+t calls ctrl_k_t_cb
}
ctrl_k_timeout () {
if (second_event_happened)
second_event_happened = FALSE;
else
ctrl_k_function ();
// reset accels to their normal state, i.e. ctrl+t calls ctrl_t_cb
}
ctrl_t_cb () {
...
}
ctrl_k_t_cb () {
second_event_happened = TRUE;
...
}
However, this approach is error-prone.
Is there a more simple or GTK-ish solution?

Related

How do I add a "save" button to the gtk filechooser dialog?

I have a Gjs app that will need to save files. I can open the file chooser dialog just fine from my menu, and I have added a "save" and "cancel" button, but I can't get the "save" button to trigger anything.
I know I'm supposed to pass it a response_id, but I'm not sure what that's supposed to look like nor what I'm supposed to do with it afterwards.
I read that part here:
https://www.roojs.com/seed/gir-1.2-gtk-3.0/gjs/Gtk.FileChooserDialog.html#expand
let actionSaveAs = new Gio.SimpleAction ({ name: 'saveAs' });
actionSaveAs.connect('activate', () => {
const saver = new Gtk.FileChooserDialog({title:'Select a destination'});
saver.set_action(Gtk.FileChooserAction.SAVE);
saver.add_button('save', 'GTK_RESPONSE_ACCEPT');
saver.add_button('cancel', 'GTK_RESPONSE_CANCEL');
const res = saver.run();
if (res) {
print(res);
const filename = saver.get_filename();
print(filename);
}
saver.destroy();
});
APP.add_action(actionSaveAs);
I can catch res and fire the associated little logging action when I close the dialog, but both the "save" and "cancel" buttons just close the dialog without doing or saying anything.
My question is, what are GTK_RESPONSE_ACCEPT and GTK_RESPONSE_CANCEL supposed to be (look like) in GJS and how do I use them?
In GJS enums like GTK_RESPONSE_* are numbers and effectively look like this:
// imagine this is the Gtk import
const Gtk = {
ResponseType: {
NONE: -1,
REJECT: -2,
ACCEPT: -3,
DELETE_EVENT: -4,
...
}
};
// access like so
let response_id = -3;
if (response_id === Gtk.ResponseType.ACCEPT) {
log(true);
}
There's a bit more information here about that.
let saver = new Gtk.FileChooserDialog({
title:'Select a destination',
// you had the enum usage correct here
action: Gtk.FileChooserAction.SAVE
});
// Really the response code doesn't matter much, since you're
// deciding what to do with it. You could pass number literals
// like 1, 2 or 3. Probably this was not working because you were
// passing a string as a response id.
saver.add_button('Cancel', Gtk.ResponseType.CANCEL);
saver.add_button('Save', Gtk.ResponseType.OK);
// run() is handy, but be aware that it will block the current (only)
// thread until it returns, so I usually prefer to connect to the
// GtkDialog::response signal and use GtkWidget.show()
saver.connect('response', (dialog, response_id) => {
if (response_id === Gtk.ResponseType.OK) {
// outputs "-5"
print(response_id);
// NOTE: we're using #dialog instead of 'saver' in the callback to
// avoid a possible cyclic reference which could prevent the dialog
// from being garbage collected.
let filename = dialog.get_filename();
// here's where you do your stuff with the filename. You might consider
// wrapping this whole thing in a re-usable Promise. Then you could call
// `resolve(filename)` or maybe `resolve(null)` if the response_id
// was not Gtk.ResponseType.OK. You could then `await` the result to get
// the same functionality as run() but allow other code to execute while
// you wait for the user.
print(filename);
// Also note, you actually have to do the writing yourself, such as
// with a GFile. GtkFileChooserDialog is really just for getting a
// file path from the user
let file = Gio.File.new_for_path(filename);
file.replace_contents_bytes_async(
// of course you actually need bytes to write, since GActions
// have no way to return a value, unless you're passing all the
// data through as a parameter, it might not be the best option
new GLib.Bytes('file contents to write to disk'),
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null,
// "shadowing" variable with the same name is another way
// to prevent cyclic references in callbacks.
(file, res) => {
try {
file.replace_contents_finish(res);
} catch (e) {
logError(e);
}
}
);
}
// destroy the dialog regardless of the response when we're done.
dialog.destroy();
});
// for bonus points, here's how you'd implement a simple preview widget ;)
saver.preview_widget = new Gtk.Image();
saver.preview_widget_active = false;
this.connect('update-preview', (dialog) => {
try {
// you'll have to import GdkPixbuf to use this
let pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
dialog.get_preview_filename(),
dialog.get_scale_factor() * 128,
-1
);
dialog.preview_widget.pixbuf = pixbuf;
dialog.preview_widget.visible = true;
dialog.preview_widget_active = true;
// if there's some kind of error or the file isn't an image
// we'll just hide the preview widget
} catch (e) {
dialog.preview_widget.visible = false;
dialog.preview_widget_active = false;
}
});
// this is how we'll show the dialog to the user
saver.show();

How to intercept system keypress in a GTK application

I am working on a Vala GTK application that starts minimized by default and I want to bind a specific keyword shortcut to bring the minimized window to the front.
I am able to handle Keyboard events using Accelerators when the app is focus ed, but I am unable to intercept any key press from the system when the app is minimized.
How can I make the app listen to system keyboard events so I can detect the key press accordingly?
Thank you.
I took a look at the source for Ideogram to see how it registers Super-e as a hot-key. It looks to be basically the following, including first checking that a custom hot-key has not already been registered for the application.
// These constants are set at class level.
public const string SHORTCUT = "<Super>e";
public const string ID = "com.github.cassidyjames.ideogram";
// Set shortcut within activate method.
CustomShortcutSettings.init ();
bool has_shortcut = false;
foreach (var shortcut in CustomShortcutSettings.list_custom_shortcuts ()) {
if (shortcut.command == ID) {
has_shortcut = true;
return;
}
}
if (!has_shortcut) {
var shortcut = CustomShortcutSettings.create_shortcut ();
if (shortcut != null) {
CustomShortcutSettings.edit_shortcut (shortcut, SHORTCUT);
CustomShortcutSettings.edit_command (shortcut, ID);
}
}
It uses a CustomShortcutSettings class included in its source to handle the reading and writing to the system settings. The class originated in another application called Clipped.

VSCode extension - how to alter file's text

I have an extension that grabs the open file's text and alters it. Once the text is altered, how do I put it back into the file that is displayed in VSCode?
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "myExtension" is now active!');
console.log(process.versions);
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
let disposable = vscode.commands.registerCommand('extension.myExtension', () => {
// The code you place here will be executed every time your command is executed
let activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
let text = activeEditor.document.getText();
getAsyncApi(text).then((textToInsertIntoDoc) => {
let finaldoc = insertTextIntoDoc(text, textToInsertIntoDoc);
// not what I want - just used to see new text
vscode.window.showInformationMessage(textToInsertIntoDoc);
});
});
context.subscriptions.push(disposable);
}
The API you can use here is TextEditor.edit, whose definition is
edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean>;
It asks for a callback as the first parameter and in the callback, you can make edits to the document by visiting editBuilder.
I put a sample extension in https://github.com/Microsoft/vscode-extension-samples/tree/master/document-editing-sample which reverses the content in current selection, which is basically a simple use TextEditor.edit.
This is a revision of the main function in Rebornix's extension sample (included with the set of Microsoft extension samples) that handles the selection issues you raised. It reverses the content of the selection(s) (leaving the selections) or if a selection is empty it will reverse the word under the cursor at that selection without leaving anything selected. It often makes sense to leave a selection, but you can add code to remove selection.
let disposable = vscode.commands.registerCommand('extension.reverseWord', function () {
// Get the active text editor
const editor = vscode.window.activeTextEditor;
if (editor) {
const document = editor.document;
editor.edit(editBuilder => {
editor.selections.forEach(sel => {
const range = sel.isEmpty ? document.getWordRangeAtPosition(sel.start) || sel : sel;
let word = document.getText(range);
let reversed = word.split('').reverse().join('');
editBuilder.replace(range, reversed);
})
}) // apply the (accumulated) replacement(s) (if multiple cursors/selections)
}
});
Admittedly, while I could remove a single selection by setting .selection to a new empty selection that doesn't seem to work with .selections[i]. But you can make multiple changes without having selections in the first place.
What you don't want to do is make a selection through code just to alter text through code. Users make selections, you don't (unless the end purpose of the function is to make a selection).
I came to this question looking for a way to apply a textEdit[] array (which is normally returned by a provideDocumentRangeFormattingEdits callback function). If you build changes in the array you can apply them to your document in your own function:
const { activeTextEditor } = vscode.window;
if (activeTextEditor) {
const { document } = activeTextEditor;
if (document) {
/*
build your textEdits similarly to the above with insert, delete, replace
but not within an editBuilder arrow function
const textEdits: vscode.TextEdit[] = [];
textEdits.push(vscode.TextEdit.replace(...));
textEdits.push(vscode.TextEdit.insert(...));
*/
const workEdits = new vscode.WorkspaceEdit();
workEdits.set(document.uri, textEdits); // give the edits
vscode.workspace.applyEdit(workEdits); // apply the edits
}
}
So that's another way to apply edits to a document. Even though I got the editBuilder sample to work correctly without selecting text, I have had problems with selections in other cases. WorkspaceEdit doesn't select the changes.
Here is the code snippet that will solve your issue :
activeEditor.edit((selectedText) => {
selectedText.replace(activeEditor.selection, newText);
})
Due to the issues I commented about in the above answer, I ended up writing a quick function that does multi-cursor friendly insert, and if the selection was empty, then it does not leave the inserted text selected afterwards (i.e. it has the same intuitive behavior as if you had pressed CTRL + V, or typed text on the keyboard, etc.)
Invoking it is simple:
// x is the cursor index, it can be safely ignored if you don't need it.
InsertText(x => 'Hello World');
Implementation:
function InsertText(getText: (i:number) => string, i: number = 0, wasEmpty: boolean = false) {
let activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) { return; }
let sels = activeEditor.selections;
if (i > 0 && wasEmpty)
{
sels[i - 1] = new vscode.Selection(sels[i - 1].end, sels[i - 1].end);
activeEditor.selections = sels; // required or the selection updates will be ignored! 😱
}
if (i < 0 || i >= sels.length) { return; }
let isEmpty = sels[i].isEmpty;
activeEditor.edit(edit => edit.replace(sels[i], getText(i))).then(x => {
InsertText(getText, i + 1, isEmpty);
});
}
let strContent = "hello world";
const edit = new vscode.WorkspaceEdit();
edit.insert(YOUR_URI, new vscode.Position(0, 0), strContent);
let success = await vscode.workspace.applyEdit(edit);

Join two observables in RX.js

I'm trying to create new observable based on two others. I have:
var mouseClickObservable = Rx.Observable.fromEvent(this.canvas, "click");
var mouseMoveObservable = Rx.Observable.fromEvent(this.canvas, "mousemove");
function findObject(x, y) {/* logic for finding object under cursor here. */}
var objectUnderCursor = this.mouseMoveObservable.select(function (ev) {
return findObject(ev.clientX, clientY);
});
I want to create objectClicked observable, that should produce values when user clicks on an object. I could just call findObject again, like this:
var objectClicked = this.mouseClickObservable.select(function (ev) {
return findObject(ev.clientX, clientY);
});
but it's very time-consuming function.
Another way, which I currently use, is to store last hovered object in a variable, but I assume, there should be pure functional way of doing this. I tryed to use Observable.join like this:
var objectClicked = this.objectUnderCursor.join(
mouseClickObservable,
function (obj) { return this.objectUnderCursor },
function (ev) { return Rx.Observable.empty() },
function (obj, ev) { return obj })
but it produces multiple values for one click
I don't see any code where you actually subscribe to any of these observables you have defined, so it is hard to provide a good answer. Do you actually need to call findObject on every mouse move? Are you needing to provide some sort of hover effect as the mouse moves? Or do you just need to know the object that was clicked, in which case you only need to call findObject once when clicked?
Assuming you only need to know what object was clicked, you don't even worry about mouse moves and just do something like:
var objectClicked = mouseClickObservable
.select(function (ev) { return findObject(ev.clientX, ev.clientY); });
objectClicked.subscribe(function(o) { ... });
If you indeed need to know what object the mouse is hovering over, but want to avoid calling your expensive hit test also on a click, then indeed you need to store the intermediate value (which you are needing to store anyway to do your hovering effects). You can use a BehaviorSubject for this purpose:
this.objectUnderCursor = new Rx.BehaviorSubject();
mouseMoveObservable
.select(function (ev) { return findObject(ev.clientX, ev.clientY); })
.subscribe(this.objectUnderCursor);
this.objectUnderCursor.subscribe(function (o) { do your hover effects here });
mouseClickObservable
.selectMany(function () { return this.objectUnderCursor; })
.subscribe(function (o) { do your click effect });

jquery regarding toggle ()

I need to toggle an element ONLY if it is not disabled.
jQuery("#sbutton").toggle(
function () {
if (!jQuery(\'input[name^="choose"]\').attr ( "disabled" )) {
jQuery(\'input[name^="choose"]\').attr ( "checked" , true);
}
},
function () {
jQuery(\'input[name^="choose"]\').removeAttr("Checked");
}
)
Is the IF condition possible?
What you probably want to do (thanks Frédéric):
jQuery("#sbutton").click(function() {
if (jQuery('input[name^="choose"]').is(':disabled'))
return false;
if (jQuery('input[name^="choose"]').is(':checked'))
jQuery('input[name^="choose"]').removeAttr("checked");
else
jQuery('input[name^="choose"]').attr("checked", true);
});
or simply
jQuery("#sbutton").click(function() {
var checkbox = jQuery('input[name^="choose"]');
if (checkbox.is(':disabled'))
return false;
checkbox.attr('checked', !checkbox.is(':checked'));
});
The problem with your code is that you expect the evaluation on disabled to be evaluated on every button click and use the first function if true. It's only called on every other click though, and the other function doesn't care if it's disabled or not. It checks the check box no matter what. You have to either bind on the click event, like I've done, or bind to and unbind from the toggle event depending on whether or not the button is disabled.
In the future it would be easier to help you if you present your code as a fiddle (http://www.jsfiddle.net) and describe more thoroughly what you're trying to do and what it is that's not working.