Related
I'm developing an app with Angular and Semantic-UI. The app should be accessible, this means it should be compliant with WCAG 2.0.
To reach this purpose the modals should keep focus within the dialog and prevents users from going outside or move with "tabs" between elements of the page that lays under the modal.
I have found some working examples, like the following:
JQuery dialog: https://jqueryui.com/dialog/#modal-confirmation
dialog HTML 5.1 element: https://demo.agektmr.com/dialog
ARIA modal dialog example:
http://w3c.github.io/aria-practices/examples/dialog-modal/dialog.html
(that I have reproduced on Plunker)
Here is my try to create an accessible modal with Semantic-UI: https://plnkr.co/edit/HjhkZg
As you can see I used the following attributes:
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
But they don't solve my issue. Do you know any way to make my modal keeping focus and lose it only when user click on cancel/confirm buttons?
There is currently no easy way to achieve this. The inert attribute was proposed to try to solve this problem by making any element with the attribute and all of it's children inaccessible. However, adoption has been slow and only recently did it land in Chrome Canary behind a flag.
Another proposed solution is making a native API that would keep track of the modal stack, essentially making everything not currently the top of the stack inert. I'm not sure the status of the proposal, but it doesn't look like it will be implemented any time soon.
So where does that leave us?
Unfortunately without a good solution. One solution that is popular is to create a query selector of all known focusable elements and then trap focus to the modal by adding a keydown event to the last and first elements in the modal. However, with the rise of web components and shadow DOM, this solution can no longer find all focusable elements.
If you always control all the elements within the dialog (and you're not creating a generic dialog library), then probably the easiest way to go is to add an event listener for keydown on the first and last focusable elements, check if tab or shift tab was used, and then focus the first or last element to trap focus.
If you're creating a generic dialog library, the only thing I have found that works reasonably well is to either use the inert polyfill or make everything outside of the modal have a tabindex=-1.
var nonModalNodes;
function openDialog() {
var modalNodes = Array.from( document.querySelectorAll('dialog *') );
// by only finding elements that do not have tabindex="-1" we ensure we don't
// corrupt the previous state of the element if a modal was already open
nonModalNodes = document.querySelectorAll('body *:not(dialog):not([tabindex="-1"])');
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (!modalNodes.includes(node)) {
// save the previous tabindex state so we can restore it on close
node._prevTabindex = node.getAttribute('tabindex');
node.setAttribute('tabindex', -1);
// tabindex=-1 does not prevent the mouse from focusing the node (which
// would show a focus outline around the element). prevent this by disabling
// outline styles while the modal is open
// #see https://www.sitepoint.com/when-do-elements-take-the-focus/
node.style.outline = 'none';
}
}
}
function closeDialog() {
// close the modal and restore tabindex
if (this.type === 'modal') {
document.body.style.overflow = null;
// restore or remove tabindex from nodes
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (node._prevTabindex) {
node.setAttribute('tabindex', node._prevTabindex);
node._prevTabindex = null;
}
else {
node.removeAttribute('tabindex');
}
node.style.outline = null;
}
}
}
The different "working examples" do not work as expected with a screenreader.
They do not trap the screenreader visual focus inside the modal.
For this to work, you have to :
Set the aria-hidden attribute on any other nodes
disable keyboard focusable elements inside those trees (links using tabindex=-1, controls using disabled, ...)
The jQuery :focusable pseudo selector can be useful to find focusable elements.
add a transparent layer over the page to disable mouse selection.
or you can use the css pointer-events: none property when the browser handles it with non SVG elements, not in IE
This focus-trap plugin is excellent at making sure that focus stays trapped inside of dialogue elements.
It sounds like your problem can be broken down into 2 categories:
focus on dialog box
Add a tabindex of -1 to the main container which is the DOM element that has role="dialog". Set the focus to the container.
wrapping the tab key
I found no other way of doing this except by getting the tabbable elements within the dialog box and listening it on keydown. When I know the element in focus (document.activeElement) is the last one on the list, I make it wrap
"focus" events can be intercepted in the capture phase, so you can listen for them at the document.body level, squelch them before they reach the target element, and redirect focus back to a control in your modal dialog. This example assumes a modal dialog with an input element gets displayed and assigned to the variable currDialog:
document.body.addEventListener("focus", (event) => {
if (currDialog && !currDialog.contains(event.target)) {
event.preventDefault();
event.stopPropagation();
currDialog.querySelector("input").focus();
}
}, {capture: true});
You may also want to contain such a dialog in a fixed-position, clear (or low-opacity) backdrop element that takes up the full screen in order to capture and suppress mouse/pointer events, so that no browser feedback (hover, etc.) occurs that could give the user the impression that the background is active.
Don't use any solution requiring you to look up "tabbable" elements. Instead, use keydown and either click events or a backdrop in an effective manor.
(Angular1)
See Asheesh Kumar's answer at https://stackoverflow.com/a/31292097/1754995 for something similar to what I am going for below.
(Angular2-x, I haven't done Angular1 in a while)
Say you have 3 components: BackdropComponent, ModalComponent (has an input), and AppComponent (has an input, the BackdropComponent, and the ModalComponent). You display BackdropComponent and ModalComponent with the correct z-index, both are currently displayed/visible.
What you need to do is have a general window.keydown event with preventDefault() to stop all tabbing when the backdrop/modal component is displayed. I recommend you put that on a BackdropComponent. Then you need a keydown.tab event with stopPropagation() to handle tabbing for the ModalComponent. Both the window.keydown and keydown.tab could probably be in the ModalComponent but there is purpose in a BackdropComponent further than just modals.
This should prevent clicking and tabbing to the AppComponent input and only click or tab to the ModalComponent input [and browser stuffs] when the modal is shown.
If you don't want to use a backdrop to prevent clicking, you can use use click events similarly to the keydown events described above.
Backdrop Component:
#Component({
selector: 'my-backdrop',
host: {
'tabindex': '-1',
'(window:keydown)': 'preventTabbing($event)'
},
...
})
export class BackdropComponent {
...
private preventTabbing(event: KeyboardEvent) {
if (event.keyCode === 9) { // && backdrop shown?
event.preventDefault();
}
}
...
}
Modal Component:
#Component({
selector: 'my-modal',
host: {
'tabindex': '-1',
'(keydown.tab)': 'onTab($event)'
},
...
})
export class ModalComponent {
...
private onTab(event: KeyboardEvent) {
event.stopPropagation();
}
...
}
Here's my solution. It traps Tab or Shift+Tab as necessary on first/last element of modal dialog (in my case found with role="dialog"). Eligible elements being checked are all visible input controls whose HTML may be input,select,textarea,button.
$(document).on('keydown', function(e) {
var target = e.target;
var shiftPressed = e.shiftKey;
// If TAB key pressed
if (e.keyCode == 9) {
// If inside a Modal dialog (determined by attribute role="dialog")
if ($(target).parents('[role=dialog]').length) {
// Find first or last input element in the dialog parent (depending on whether Shift was pressed).
// Input elements must be visible, and can be Input/Select/Button/Textarea.
var borderElem = shiftPressed ?
$(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first()
:
$(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
if ($(borderElem).length) {
if ($(target).is($(borderElem))) {
return false;
} else {
return true;
}
}
}
}
return true;
});
we can use the focus trap npm package.
npm i focus-trap
This might help someone who is looking for solution in Angular.
Step 1: Add keydown event on dialog component
#HostListener('document:keydown', ['$event'])
handleTabKeyWInModel(event: any) {
this.sharedService.handleTabKeyWInModel(event, '#modal_id', this.elementRef.nativeElement, 'input,button,select,textarea,a,[tabindex]:not([tabindex="-1"])');
}
This will filters the elements which are preseneted in the Modal dialog.
Step 2: Add common method to handle focus in shared service (or you can add it in your component as well)
handleTabKeyWInModel(e, modelId: string, nativeElement, tagsList: string) {
if (e.keyCode === 9) {
const focusable = nativeElement.querySelector(modelId).querySelectorAll(tagsList);
if (focusable.length) {
const first = focusable[0];
const last = focusable[focusable.length - 1];
const shift = e.shiftKey;
if (shift) {
if (e.target === first) { // shift-tab pressed on first input in dialog
last.focus();
e.preventDefault();
}
} else {
if (e.target === last) { // tab pressed on last input in dialog
first.focus();
e.preventDefault();
}
}
}
}
}
Now this method will take the modal dialog native element and start evaluate on every tab key. Finally we will filter the event on first and last so that we can focus on appropriate elements (on first after last element tab click and on last shift+tab event on first element).
Happy Coding.. :)
I used one of the methods suggested by Steven Lambert, namely, listening to keydown events and intercepting "tab" and "shift+tab" keys. Here's my sample code (Angular 5):
import { Directive, ElementRef, Attribute, HostListener, OnInit } from '#angular/core';
/**
* This directive allows to override default tab order for page controls.
* Particularly useful for working around the modal dialog TAB issue
* (when tab key allows to move focus outside of dialog).
*
* Usage: add "custom-taborder" and "tab-next='next_control'"/"tab-prev='prev_control'" attributes
* to the first and last controls of the dialog.
*
* For example, the first control is <input type="text" name="ctlName">
* and the last one is <button type="submit" name="btnOk">
*
* You should modify the above declarations as follows:
* <input type="text" name="ctlName" custom-taborder tab-prev="btnOk">
* <button type="submit" name="btnOk" custom-taborder tab-next="ctlName">
*/
#Directive({
selector: '[custom-taborder]'
})
export class CustomTabOrderDirective {
private elem: HTMLInputElement;
private nextElemName: string;
private prevElemName: string;
private nextElem: HTMLElement;
private prevElem: HTMLElement;
constructor(
private elemRef: ElementRef
, #Attribute('tab-next') public tabNext: string
, #Attribute('tab-prev') public tabPrev: string
) {
this.elem = this.elemRef.nativeElement;
this.nextElemName = tabNext;
this.prevElemName = tabPrev;
}
ngOnInit() {
if (this.nextElemName) {
var elems = document.getElementsByName(this.nextElemName);
if (elems && elems.length && elems.length > 0)
this.nextElem = elems[0];
}
if (this.prevElemName) {
var elems = document.getElementsByName(this.prevElemName);
if (elems && elems.length && elems.length > 0)
this.prevElem = elems[0];
}
}
#HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
if (event.key !== "Tab")
return;
if (!event.shiftKey && this.nextElem) {
this.nextElem.focus();
event.preventDefault();
}
if (event.shiftKey && this.prevElem) {
this.prevElem.focus();
event.preventDefault();
}
}
}
To use this directive, just import it to your module and add to Declarations section.
I've been successful using Angular Material's A11yModule.
Using your favorite package manager install these to packages into your Angular app.
**"#angular/material": "^10.1.2"**
**"#angular/cdk": "^10.1.2"**
In your Angular module where you import the Angular Material modules add this:
**import {A11yModule} from '#angular/cdk/a11y';**
In your component HTML apply the cdkTrapFocus directive to any parent element, example: div, form, etc.
Run the app, tabbing will now be contained within the decorated parent element.
For jquery users:
Assign role="dialog" to your modal
Find first and last interactive element inside the dialog modal.
Check if current target is one of them(depending on shift key is
pressed or not).
If target element is one of first or last interactive element of the
dialog, return false
Working code sample:
//on keydown inside dialog
$('.modal[role=dialog]').on('keydown', e => {
let target = e.target;
let shiftPressed = e.shiftKey;
// If TAB is pressed
if (e.keyCode === 9) {
// Find first and last element in the ,modal-dialog parent.
// Elements must be interactive i.e. visible, and can be Input/Select/Button/Textarea.
let first = $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first();
let last = $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
let borderElem = shiftPressed ? first : last //border element on the basis of shift key pressed
if ($(borderElem).length) {
return !$(target).is($(borderElem)); //if target is border element , return false
}
}
return true;
});
I read through most of the answers, while the package focus-trap seems like a good option. #BenVida shared a very simple VanillaJS solution here in another Stack Overflow post.
Here is the code:
const container=document.querySelector("_selector_for_the_container_")
//optional: needed only if the container element is not focusable already
container.setAttribute("tabindex","0")
container.addEventListener("focusout", (ev)=>{
if (!container.contains(ev.relatedTarget)) container.focus()
})
I have some dependencies hierarchy on my form, so I implemented hierarchy check on server side of the scout. If one field is changed, it triggered check if other need to be changed as well. This is done with export/import form data.
MyFormData input = new MyFormData();
FormDataUtility.exportFormData(this, input);
input = BEANS.get(IMYService.class).validate(input, field);
FormDataUtility.importFormFieldData(this, input, false, null, null);
validate function change all other fields that need to be changed.
My problem is with editing cells in editable tables.
If I change value in cell, and this chain validation is triggered, after importing form data I lose focus in cell. So instead, tab will move me to another cell, tab trigger import and focus in cells are lost. And this is a really bad user experience.
How to fix this?
How to stay in focus (of next cell) after import has been called?
Marko
I am not sure, if this applies for you, but you can try the following:
I assume, that you do your export/validate/import logic in the execCompleteEdit(ITableRow row, IFormField editingField) of your column class. I suggest, that you calculate your next focusable cell by yourself and request its focus after importing the form data.
As an example, you can do this like that:
#Override
protected void execCompleteEdit(ITableRow row, IFormField editingField) {
super.execCompleteEdit(row, editingField);
// create form data object
// export form data
// call service and validate
// import form data
// request focus for next cell
focusNextAvailableCell(this, row);
}
with focusNextAvailableCell(this, row) as following:
private void focusNextAvailableCell(IColumn<?> col, ITableRow row) {
if (col == null || row == null) {
return;
}
IColumn<?> nextColumn = getColumnSet().getColumn(col.getColumnIndex()+1);
ITableRow nextRow = getTable().getRow(row.getRowIndex());
if (nextColumn == null) {
// no next column (last column lost focus)
// check if next row is available
nextRow = getTable().getRow(row.getRowIndex()+1);
// maybe select first cell again?
if (nextRow == null) {
nextColumn = getColumnSet().getColumn(0);
nextRow = getTable().getRow(0);
}
}
if (nextColumn != null && nextRow != null) {
getTable().requestFocusInCell(nextColumn, nextRow);
}
}
You should be aware, that you have to call this after every form data import in your execCompleteEdit method of your column. Also this is triggered not only when switching cells through pressing the tab key, but also when clicking with the mouse button.
Best regards!
I am facing problem with lastButton of SimplePager.
I have 3 pages in celltable, Page size=11 (1 empty record + 10 records(with value)), Total record=26.
I used CustomerPager by extending SimplePager.
In 1st attempt 1+10 records display in celltable : Next & Last page button is enabled (First & Prev button disabled) which is correct.
But LastPage button not working... :( Dont know whats the issue... (event not fires)
Strange behavior:
#1 Last page button is working only when I visit to last page(3 page in my case).
#2 Assume I am on 1st page n I moved to 2nd page(Total 3 pages in celltable). that time all buttons are enabled which is correct.
In this case Last button is working but behave like Next Button
My GWT application integrated into one of our product so cant debug it from client side.
May be index value is improper in setPage(int index) method from AbstractPager
Code flow is as follows for Last button
//From SimplePager
lastPage.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
lastPage();
}
});
#Override
public void lastPage() {
super.lastPage();
}
// From AbstractPager
/**
* Go to the last page.
*/
protected void lastPage() {
setPage(getPageCount() - 1);
}
protected void setPage(int index) {
if (display != null && (!isRangeLimited || !display.isRowCountExact() || hasPage(index))) {
// We don't use the local version of setPageStart because it would
// constrain the index, but the user probably wants to use absolute page
// indexes.
int pageSize = getPageSize();
display.setVisibleRange(pageSize * index, pageSize);
}
}
or may be some conditions false from above code(from setPage())
actual record = 26 and 3 Empty record (1st Empty record/page)
May b problem with dataSize :|
How I can check number of pages based on the data size?
?
How can I solve this problem?
edit: I found out that the default constructor of the pager doesn't give you a "last" button, but a "fast forward 1000 lines" button instead (horrible, right?) .
call the following constructor like so, and see your problem solved:
SimplePager.Resources resources = GWT.create(SimplePager.Resources.class);
SimplePager simplePager = new SimplePager(TextLocation.CENTER, resources , false, 1000, true);
the first "false" flag turns off the "fastforward button" and the last "true" flag turns on the "last" button.
also the last button will work only if the pager knows the total amount of records you have.
you can call the table's setRowCount function to update the total like so:
int totalRecordsSize = 26; //the total amount of records you have
boolean isTotalExact = true; //is it an estimate or an exact match
table.setRowCount(totalRecordsSize , isTotalExact); //sets the table's total and updates the pager (assuming you called pager.setDisplay(table) before)
if you are working with an attached DataProvider, than all it's updateRowCount method instead (same usage).
Without seeing more of your code, this is a hard question to answer as there could be multiple places where things are going wrong.
I would make sure you call setDisplay(...) on your SimplePager so it has the data it needs calculate its ranges.
If you can't run in devmode, I recommend setting up some GWT logging in the browser (write the logs to a popup panel or something, see this discussion for an example).
I think the problem is related with condition in the setPage(). Try putting SOP before if condition or debug the code
Only added cellTable.setRowCount(int size, boolean isExact) in OnRange change method AsyncDataProvider. My problem is solved :)
protected void onRangeChanged(HasData<RecordVO> display) {
//----- Some code --------
cellTable.setRowCount(searchRecordCount, false);
//----- Some code --------
}
The tutorial here http://developer.gnome.org/gtk-tutorial/2.90/x542.html
shows how to set up the radio buttons, but neglects to tell you how to use them.
How do I then find which radio button is selected?
My solution:
Initialise radio buttons with:
rbutton1 = gtk_radio_button_new_with_label(NULL, "button1");
gtk_box_pack_start(GTK_BOX(rbutton_box), rbutton1, TRUE, TRUE, 0);
rbuttonGroup = gtk_radio_button_get_group(GTK_RADIO_BUTTON(rbutton1)); /*not sure what I'd use this line for currently though*/
rbutton2 = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(rbutton1), "button 2");
gtk_box_pack_start(GTK_BOX(rbutton_box), rbutton2, TRUE, TRUE, 0);
rbutton3 = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(rbutton1), "button 3");
gtk_box_pack_start(GTK_BOX(rbutton_box), rbutton3, TRUE, TRUE, 0);
And update a variable telling you which radio button is selected with this method:
void checkRadioButtons()
{
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(rbutton1))==TRUE) selectedRadioButton =1;
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(rbutton2))==TRUE) selectedRadioButton =2;
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(rbutton3))==TRUE) selectedRadioButton =3;
}
Google brought me here for python / pygtk / pygtk3 searches, so I hope its okay that I post a pygtk solution:
def _resolve_radio(self, master_radio):
active = next((
radio for radio in
master_radio.get_group()
if radio.get_active()
))
return active
This uses a generator to return the first (which should be the only) active radio box that is active.
This is how I do it.
GtkRadioButton * radio_button;
GtkRadioButton * radio_button1;
GtkRadioButton * radio_button2;
...
GSList * tmp_list = gtk_radio_button_get_group (radio_button);//Get the group of them.
GtkToggleButton *tmp_button = NULL;//Create a temp toggle button.
while (tmp_list)//As long as we didn't reach the end of the group.
{
tmp_button = tmp_list->data;//Get one of the buttons in the group.
tmp_list = tmp_list->next;//Next time we're going to check this one.
if (gtk_toggle_button_get_active(tmp_button))//Is this the one active?
break;//Yes.
tmp_button = NULL;//We've enumerated all of them, and none of them is active.
}
//Here. tmp_button holds the active one. NULL if none of them is active.
See the discussion here.
I don't know if they will add this function into it (seems not).
Here's how I suggest doing it:
void radio_button_selected (GtkWidget *widget, gpointer data)
{
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))
{
GSLIST *group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (widget));
g_print ("Index = %i%\n", g_slist_index (group, widget));
}
}
You may connect to the GtkToggleButton::toggled signal instead. In the associated callback, you'll be able to update your variable. As for the call to gtk_radio_button_get_group, you only need it if you call gtk_radio_button_new_with_label instead of gtk_radio_button_new_with_label_with_widget, as specified in the tutorial you're refering to.
Let's create a serie of buttons :
for severity in levels:
radio = gtk.RadioButton(group=radioButtons, label=severity)
if severity == actualLevel:
radio.set_active(True)
hBox.pack_start(radio, True, True, 3)
radio.connect('toggled', self.radioButtonSelected, severity)
and all buttons are connected to the same handler :
def radioButtonSelected(self, button, currentSeverity):
# proceed with the task
# as you can see, button is passed by as argument by the event handler
# and you can, for example, get the button label :
labelReadFromButton = button.getLabel()
Use lambda expressions if you dont want to mess around with the annoying methods, still have to use connect though, but its alot easier to read:
Enum RadioValues { A, B, C, none };
RadioValues values = RadioValues.none; // only needed if you dont have an initially selected radio button
MyConstructor()
{
Build();
// asumming you have 3 radio buttons: radioA, radioB, radioC:
radioA.Toggled += (sender,e) => values = RadioValues.A;
radioB.Toggled += (sender,e) => values = RadioValues.B;
radioC.Toggled += (sender,e) => values = RadioValues.C;
}
and thats it, no methods to deal with, and you dont have to restrict yourself to just that either, you can also use an anonymous function if you need more flex--of course the next step after that is using methods. Unfortunately they didnt offer a simple .Checked property, my next suggestion is to override the radio button itself and chain a Checked property when it's toggled state is changed, emulating other frameworks like MFC, Qt, and Winforms... etc.
PS: I left out boilerplate code for simplicity's sake, which can make answers a bit more muddled and you probably just want the facts not a demonstration on whether or not I can properly call a constructor :)
My solution for GTKmm is quite easier,
You just have to call the function :
my_radio_button.get_active(); \n
This will return either 0 if its unactive or 1 if its active.
This is a demo code using Radio Buttons, where you can find how I find which radio button is selected:
#include <gtkmm/window.h>
#include <gtkmm/box.h>
#include <gtkmm/radiobutton.h>
#include <gtkmm/separator.h>
#include <gtkmm/application.h>
#include <iostream>
class ButtonWindow : public Gtk::Window
{
private:
//Child widgets:
Gtk::Box m_Box_Top, m_Box1, m_Box2;
Gtk::RadioButton m_RadioButton1, m_RadioButton2, m_RadioButton3;
Gtk::Separator m_Separator;
Gtk::Button m_Button_Close;
Gtk::RadioButton *m_SelectedButton{nullptr};
public:
ButtonWindow()
: m_Box_Top(Gtk::ORIENTATION_VERTICAL),
m_Box1(Gtk::ORIENTATION_VERTICAL, 15),
m_Box2(Gtk::ORIENTATION_VERTICAL, 0),
m_RadioButton1("button 1"),
m_RadioButton2("button 2"),
m_RadioButton3("button 3"),
m_Button_Close("close")
{
// Set title and border of the window
set_title("radio buttons");
set_border_width(0);
// Put radio buttons 2 and 3 in the same group as 1:
m_RadioButton2.join_group(m_RadioButton1);
m_RadioButton3.join_group(m_RadioButton1);
// Add outer box to the window (because the window
// can only contain a single widget)
add(m_Box_Top);
//Put the inner boxes and the separator in the outer box:
m_Box_Top.pack_start(m_Box1);
m_Box_Top.pack_start(m_Separator);
m_Box_Top.pack_start(m_Box2);
// Set the inner boxes' borders
m_Box1.set_border_width(20);
m_Box2.set_border_width(10);
// Put the radio buttons in Box1:
m_Box1.pack_start(m_RadioButton1);
m_Box1.pack_start(m_RadioButton2);
m_Box1.pack_start(m_RadioButton3);
// Put Close button in Box2:
m_Box2.pack_start(m_Button_Close);
// Connect the button signals:
#if 1 // Full C++11: (change this to #if 0 to use the traditional way)
m_RadioButton1.signal_clicked().connect([&]{on_radio_button_clicked(m_RadioButton1);});
m_RadioButton2.signal_clicked().connect([&]{on_radio_button_clicked(m_RadioButton2);});
m_RadioButton3.signal_clicked().connect([&]{on_radio_button_clicked(m_RadioButton3);});
m_Button_Close.signal_clicked().connect([&]{on_close_button_clicked();});
#else // Traditional:
m_RadioButton1.signal_clicked() // Full sigc
.connect(sigc::bind(sigc::mem_fun(*this, &ButtonWindow::on_radio_button_clicked),
sigc::ref(m_RadioButton1)));
m_RadioButton2.signal_clicked() // sigc && C++98
.connect(std::bind(sigc::mem_fun(*this, &ButtonWindow::on_radio_button_clicked),
std::ref(m_RadioButton2)));
m_RadioButton3.signal_clicked() // Full C++98
.connect(std::bind(&ButtonWindow::on_radio_button_clicked, this,
std::ref(m_RadioButton3)));
m_Button_Close.signal_clicked()
.connect(sigc::mem_fun(*this, &ButtonWindow::on_close_button_clicked));
#endif
// Set the second button active:
m_RadioButton2.set_active();
// Make the close button the default widget:
m_Button_Close.set_can_default();
m_Button_Close.grab_default();
// Show all children of the window:
show_all_children();
}
protected:
//Signal handlers:
void on_radio_button_clicked(Gtk::RadioButton& button)
{
if(m_SelectedButton != &button && button.get_active())
{
m_SelectedButton = &button;
std::cout << "Radio "<< m_SelectedButton->get_label() << " selected.\n";
}
}
void on_close_button_clicked()
{
hide(); // Close the application
}
};
int main(int argc, char *argv[])
{
auto app = Gtk::Application::create(argc, argv, "org.gtkmm.example");
ButtonWindow button;
//Shows the window and returns when it is closed.
return app->run(button);
}
I am using a TableViewer with a content provider, label provider, a ICellModifier and TextCellEditors for each column.
How can I add arrow key navigation and cell editing when the user selects the cell? I would like this to be as natural a behavior as possible.
After looking at some of the online examples, there seems to be an old way (with a TableCursor) and a new way (TableCursor does not mix with CellEditors??).
Currently, my TableViewer without a cursor will scroll in the first column only. The underlying SWT table is showing cursor as null.
Is there a good example of TableViewer using CellEditors and cell navigation via keyboard?
Thanks!
I don't know if there is a good example. I use a cluster of custom code to get what I would consider to be basic table behaviors for my application working on top of TableViewer. (Note that we are still targetting 3.2.2 at this point, so maybe things have gotten better or have otherwise changed.) Some highlights:
I do setCellEditors() on my TableViewer.
On each CellEditor's control, I establish what I consider to be an appropriate TraverseListener. For example, for text cells:
cellEditor = new TextCellEditor(table, SWT.SINGLE | getAlignment());
cellEditor.getControl().addTraverseListener(new TraverseListener() {
public void keyTraversed(TraverseEvent e) {
switch (e.detail) {
case SWT.TRAVERSE_TAB_NEXT:
// edit next column
e.doit = true;
e.detail = SWT.TRAVERSE_NONE;
break;
case SWT.TRAVERSE_TAB_PREVIOUS:
// edit previous column
e.doit = true;
e.detail = SWT.TRAVERSE_NONE;
break;
case SWT.TRAVERSE_ARROW_NEXT:
// Differentiate arrow right from down (they both produce the same traversal #*$&#%^)
if (e.keyCode == SWT.ARROW_DOWN) {
// edit same column next row
e.doit = true;
e.detail = SWT.TRAVERSE_NONE;
}
break;
case SWT.TRAVERSE_ARROW_PREVIOUS:
// Differentiate arrow left from up (they both produce the same traversal #*$&#%^)
if (e.keyCode == SWT.ARROW_UP) {
// edit same column previous row
e.doit = true;
e.detail = SWT.TRAVERSE_NONE;
}
break;
}
}
});
(For drop-down table cells, I catch left and right arrow instead of up and down.)
I also add a TraverseListener to the TableViewer's control whose job it is to begin cell editing if someone hits "return" while an entire row is selected.
// This really just gets the traverse events for the TABLE itself. If there is an active cell editor, this doesn't see anything.
tableViewer.getControl().addTraverseListener(new TraverseListener() {
public void keyTraversed(TraverseEvent e) {
if (e.detail == SWT.TRAVERSE_RETURN) {
// edit first column of selected row
}
}
});
Now, how exactly I control the editing is another story. In my case, my whole TableViewer (and a representation of each column therein) is loosely wrapped up in a custom object with methods to do what the comments above say. The implementations of those methods ultimately end up calling tableViewer.editElement() and then checking tableViewer.isCellEditorActive() to see if the cell was actually editable (so we can skip to the next editable one if not).
I also found it useful to be able to programmatically "relinquish editing" (e.g. when tabbing out of the last cell in a row). Unfortunately the only way I could come up with to do that is a terrible hack determined to work with my particular version by spelunking through the source for things that would produce the desired "side effects":
private void relinquishEditing() {
// OMG this is the only way I could find to relinquish editing without aborting.
tableViewer.refresh("some element you don't have", false);
}
Sorry I can't give a more complete chunk of code, but really, I'd have to release a whole mini-project of stuff, and I'm not prepared to do that now. Hopefully this is enough of a "jumpstart" to get you going.
Here is what has worked for me:
TableViewerFocusCellManager focusCellManager = new TableViewerFocusCellManager(tableViewer,new FocusCellOwnerDrawHighlighter(tableViewer));
ColumnViewerEditorActivationStrategy actSupport = new ColumnViewerEditorActivationStrategy(tableViewer) {
protected boolean isEditorActivationEvent(ColumnViewerEditorActivationEvent event) {
return event.eventType == ColumnViewerEditorActivationEvent.TRAVERSAL
|| event.eventType == ColumnViewerEditorActivationEvent.MOUSE_DOUBLE_CLICK_SELECTION
|| (event.eventType == ColumnViewerEditorActivationEvent.KEY_PRESSED && event.keyCode == SWT.CR)
|| event.eventType == ColumnViewerEditorActivationEvent.PROGRAMMATIC;
}
};
I can navigate in all directions with tab while editing, and arrow around when not in edit mode.
I got it working based on this JFace Snippet, but I had to copy a couple of related classes also:
org.eclipse.jface.snippets.viewers.TableCursor
org.eclipse.jface.snippets.viewers.CursorCellHighlighter
org.eclipse.jface.snippets.viewers.AbstractCellCursor
and I don't remember exactly where I found them. The is also a org.eclipse.swt.custom.TableCursor, but I couldn't get that to work.
Have a look at
Example of enabling Editor Activation on a Double Click.
The stuff between lines [ 110 - 128 ] add a ColumnViewerEditorActivationStrategy and TableViewerEditor. In my case the I wanted a single click to begin editing so i changed line 115 from:
ColumnViewerEditorActivationEvent.MOUSE_DOUBLE_CLICK_SELECTION
to ColumnViewerEditorActivationEvent.MOUSE_CLICK_SELECTION. After adding this to my TableViewer, the tab key would go from field to field with the editor enabled.