Don't set a Gtk.TreeView's selection when focusing? - python

The following code displays a window with a button and tree view. A handle for the 'clicked' signal is attached to the button and focuses the tree view. When the window is initially displayed, the tree selection has no selected items, but when the tree view receives focus, the first item is automatically selected. Is there a way to keep a selection from being made when the tree view receives focus?
Before click, button has focus and tree selection has no selected items. After click, tree view has focus, but an item has been selected.
The issue that arises from this is that I have an interface that keeps some things in sync by attaching to the 'changed' signal on the tree selection of the tree view. When the window is displayed, depending on where the tree views are in the interface, they may receive focus by default. That causes a 'changed' signal, and unexpected synchronization happens. It's possible to call set_can_focus(False) for all the tree views, but that:
only prevents keyboard cycling focus, not programmatic focus, and the selection still turns on with programmatic focus; and
seems to disable the ability to deselect a selection (e.g., by control-clicking on a row).
Similarly I can use grab_default to ensure that something else gets focus first when the window is displayed, but it doesn't keep a stray focus event from making an unexpected selection.
Based on a posted answer that says that says that selection mode SINGLE "requires at least one item to be selected", and that that explains why an element is selected on focus, I looked more into the selection mode constants. Of these, SINGLE and BROWSE seem most relevant. The pygtk documentation, GTK Selection Mode Constants, only says that:
gtk.SELECTION_SINGLE A single selection allowed by clicking.
gtk.SELECTION_BROWSE A single selection allowed by browsing with the pointer.
The GTK+3 documentation, enum GtkSelectionMode, goes into a bit more detail:
GTK_SELECTION_SINGLE Zero or one element may be selected.
GTK_SELECTION_BROWSE Exactly one element is selected. In some
circumstances, such as initially or during a search operation, it’s
possible for no element to be selected with GTK_SELECTION_BROWSE. What
is really enforced is that the user can’t deselect a currently
selected element except by selecting another element.
I don't see anything here to suggest that at least one element must be selected when the selection mode is SINGLE.
Here's code to reproduce the window and serve as an example.
from gi.repository import Gtk
# A ListStore with some words
list_store = Gtk.ListStore(str)
for selection in "Can a machine think?".split():
list_store.append([selection])
# A TreeView with a single column
tree_view = Gtk.TreeView(model=list_store)
cell_renderer = Gtk.CellRendererText()
tree_view_column = Gtk.TreeViewColumn(cell_renderer=cell_renderer,text=0,title='Words')
tree_view.append_column(tree_view_column)
# A button to focus the list
focus = Gtk.Button(label='Focus List')
focus.connect('clicked',lambda *_: tree_view.grab_focus())
# A Box to hold everything, and a Window for the Box.
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.add(focus) # button on top gets initial focus
box.add(tree_view) # tree_view below doesn't, and has no selected items
window = Gtk.Window()
window.add(box)
window.show_all()
Gtk.main()

Looking at the source in root/gtk/gtktreeview.c for tree_view.grab_focus(), we can see that gtk_tree_view_focus_to_cursor always gets called, and selects the first element. You can work around this, in some cases, though.
This is a nasty hack.
It overrides the grab_focus method, stores the selection before calling grab_focus, and clears the selection afterwards if there was no selection before.
def tree_view_grab_focus():
selection = tree_view.get_selection()
_, selected = selection.get_selected()
Gtk.TreeView.grab_focus(tree_view)
if selected is None:
selection.unselect_all()
tree_view.grab_focus = tree_view_grab_focus
Unfortunately it only applies when calling grab_focus from Python, other callers (such as GTK's keyboard navigation) don't.

Related

How can I shift select multiple items in TKinter listbox?

I am trying to select multiple items from a Listbox, it seems intuitive to press shift while selecting and have a block of items be selected, but there seems to be no build in functionality for this in Tkinter.
So I'm trying to implement it on my own, by registering the shift key and getting the latest selection. But I get stuck trying to figure out the latest selection in the Listbox. listbox.get(ACTIVE) seems to be one behind what I expect.
Here is what I have tried to do so far, I am aware that I need to do more when I know the newest selection but that will come later.
from Tkinter import *
class GUI():
def __init__(self,frame): # Some Init
self.listbox = Listbox(root, height=20, width=51, selectmode=MULTIPLE, exportselection=0, yscrollcommand=yscrollbar.set, xscrollcommand=xscrollbar.set)
# -- Some Grid setup here --
self.listbox.bind("<<ListboxSelect>>", self.selectionCallback)
frame.bind("<Shift_L>", self.shiftCallback)
frame.bind("<KeyRelease-Shift_L>", self.shiftCallback)
def selectionCallback(self,event):
print self.listbox.get(ACTIVE) # This is where im stuck
def shiftCallback(self,event):
if event.type is 2: #KeyPress
self.shift = True
elif event.type is 3: #KeyRelease
self.shift = False
if __name__ == "__main__":
root = Tk()
GUI(root)
The behavior you seem to want actually is available by default, use
Listbox(..., selectmode=EXTENDED, ...)
From effbot:
The listbox offers four different selection modes through the selectmode option. These are SINGLE (just a single choice), BROWSE (same, but the selection can be moved using the mouse), MULTIPLE (multiple item can be choosen, by clicking at them one at a time), or EXTENDED (multiple ranges of items can be chosen, using the Shift and Control keyboard modifiers). The default is BROWSE. Use MULTIPLE to get “checklist” behavior, and EXTENDED when the user would usually pick only one item, but sometimes would like to select one or more ranges of items.
As for listbox.get(ACTIVE), the item that is ACTIVE is the one that is underlined. You can see that this is only updated upon release of the mouse button. Because the <<ListboxSelect>> event is triggered on the mouse press, you get the previously selected item, because the ACTIVE is not updated yet.

ipywidget interactive hiding visibility

I would like to make an interactive module with ipywidgets.
So far so good but I'm stuck.
I want to hide the visibility of a certain ipywidget object dependent on a certain situation, and I want my printed text to show up above the widget and stay there.
dropdown=widgets.Dropdown(
options={'Coffee machine': 1, 'Washing machine': 2, 'Water Heater': 3, 'Heating System': 4, 'Dryer': 5, 'Oven': 6, 'Microwave': 7, 'Other':8},
value=1,
description='Apparaat:',
)
text_new=widgets.Text()
def text_field(value):
if(value==8):
display(text_new)
text_new.on_submit(handle_submit)
else:
text_new.visible(False) #Doesn't work but I want something like this
print("Today you had an increase in electricity consumption, would you like to name this device?") #This just be above the dropdown menu and be stuck
i=widgets.interactive(text_field, value=dropdown)
display(i)
What this does now:
When "Other" is checked in the dropdown menu, a text box appears where the user can type something.
However, when checking another machine, the text box stays there.
I just need a "hide" function but I can't seem to find one that works.
Also, after checking another option on the dropdown, the print dissapears, not coming back.
Had same problem so i found in
boton.layout.visibility = 'hidden'
or
check.layout.display = 'none'
they made some changes... i got if from here
Cannot create a widget whose initial state is visible=False
Given a widget:
import ipywidgets
button = ipywidgets.Button()
There are two direct ways to hide the the widget, with a notable difference.
Hide and unhide the widget without affecting overall page layout:
# Turn the widget "invisible" without affecting layout
button.layout.visibility = "hidden"
# Make the widget visible again, layout unaffected
button.layout.visibility = "visible"
Hide and unhide the widget and collapse the space that the widget took up:
# Hide widget and collapse empty space
button.layout.display = "none"
# Re-add the widget, adjusting page layout as necessary.
button.layout.display = "block"
When to use each one? As a rule of thumb, use layout.visibility so the page layout is not constantly jumping around as visibility is toggled. However, for very large widgets, consider using layout.display to avoid huge blank spaces.
For more general CSS information that applies here, see What is the difference between visibility:hidden and display:none?
In addition to the accepted answer, if you want to dynamically change the visibility of a control, you can declare the layout variable and reuse.
layout_hidden = widgets.Layout(visibility = 'hidden')
layout_visible = widgets.Layout(visibility = 'visible')
Like attach to an event:
def visible_txt(b):
text_box.layout = layout_visible
def hidden_txt(b):
text_box.layout = layout_hidden
btn_visible.on_click(visible_txt)
btn_hidden.on_click(hidden_txt)

Tkinter listbox change highlighted item programmatically

I have a listbox in Tkinter and I would like to change the item selected programatically when the user presses a key button. I have the keyPressed method but how do I change the selection in the Listbox in my key pressed method?
Because listboxes allow for single vs. continuous vs. distinct selection, and also allow for an active element, this question is ambiguous. The docs explain all the different things you can do.
The selection_set method adds an item to the current selection. This may or may not unselect other items, depending on your selection mode.
If you want to guarantee that you always get just that one item selected no matter what, you can clear the selection with selection_clear(0, END), then selection_set that one item.
If you want to also make the selected item active, also call activate on the item after setting it.
To understand about different selection modes, and how active and selected interact, read the docs.
If you need ListboxSelect event to be also triggered, use below code:
# create
self.lst = tk.Listbox(container)
# place
self.lst.pack()
# set event handler
self.lst_emails.bind('<<ListboxSelect>>', self.on_lst_select)
# select first item
self.lst.selection_set(0)
# trigger event manually
self.on_lst_select()
# event handler
def on_lst_select(self, e = None):
# Note here that Tkinter passes an event object to handler
if len(self.lst.curselection()) == 0:
return
index = int(self.lst.curselection()[0])
value = self.lst.get(index)
print (f'new item selected: {(index, value)}')

How do I change the built-in button labels on a gtk.Assistant?

I want to change the label of the 'Apply' button of a gtk.Assistant to 'Start'. I can't find the corresponding gtk.Button widget in the Assistant instance.
Here's some basic code for a two-page Assistant:
import gtk
a = gtk.Assistant()
page = gtk.CheckButton("Something optional")
a.append_page(page)
a.set_page_type(page, gtk.ASSISTANT_PAGE_CONTENT)
a.set_page_title(page, "Make decisions")
a.set_page_complete(page, True)
page = gtk.Label("Alright, let's build some foo.")
a.append_page(page)
a.set_page_type(page, gtk.ASSISTANT_PAGE_CONFIRM)
a.set_page_title(page, "Confirm")
a.set_page_complete(page, True)
a.connect('delete-event', gtk.main_quit)
a.connect('close', gtk.main_quit)
a.show_all()
gtk.main()
On the final page you'll see the 'Apply' button. I want to change that text to 'Start'.
gtk.Assistant.children() and .get_children() return the list of page widgets.
gtk.Assistant.get_child() returns None.
gtk.Assistant.get_action_area() isn't a method.
Here's a link to the documentation.: http://www.pygtk.org/docs/pygtk/class-gtkassistant.html
How do I find the gtk.Button I'm interested in?
I managed to find a solution while experimenting with workarounds.
gtk.Assistant overrides the gtk.Container.get_children() method with something that returns the list of pages, but it is still in fact the parent of a gtk.HBox() which contains the buttons for 'Next', 'Apply', 'Cancel', etc.
The method gtk.Assistant.add_action_widget() adds a widget to the so-called "action area". It turns out this is the HBox containing the relevant buttons. The following function will produce a reference to the HBox:
def get_buttons_hbox(assistant):
# temporarily add a widget to the action area and get its parent
label = gtk.Label('')
assistant.add_action_widget(label)
hbox = label.get_parent()
hbox.remove(label)
return hbox
Then the buttons are retrieved using get_buttons_hbox(a).get_children().
for child in get_buttons_hbox(a).get_children():
print child.get_label()
This prints:
gtk-goto-last
gtk-go-back
gtk-go-forward
gtk-apply
gtk-cancel
gtk-close
So the following code solves the problem (using get_buttons_hbox() defined above):
for child in get_buttons_hbox(a).get_children():
label = child.get_label()
if label == 'gtk-apply':
child.set_label('Start')
I'm not sure this will be possible with pygtk. If you switch to GObject Introspection with python you can set a fully custom action area. From the Gtk3 GtkAssistant documentation:
If you have a case that doesn't quite fit in GtkAssistants way of
handling buttons, you can use the GTK_ASSISTANT_PAGE_CUSTOM page type
and handle buttons yourself.
and
GTK_ASSISTANT_PAGE_CUSTOM Used for when other page types are not
appropriate. No buttons will be shown, and the application must add
its own buttons through gtk_assistant_add_action_widget().

Can I exclude specific controllers from the standard tab traversal?

I've made a custom dialog that contains a series of text controls. Every text control has a couple of buttons beside them for adding specific values more conveniently. I don't want these buttons to receive focus when the user it tab traversing through the dialog, since the user, in most cases, won't need to use the buttons.
Is there any convenient way to exclude specific controllers from the standard tab traversal?
A simple way to prevent a button from being focused with the keyboard is to derive from wx.lib.buttons.GenButton or wx.lib.buttons.ThemedGenButton which are based on wx.PyControl that supports overriding of AcceptsFocusFromKeyboard():
class NoFocusButton(wx.lib.buttons.ThemedGenButton):
def __init__(self, parent, id=wx.ID_ANY, label=wx.EmptyString, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, validator=wx.DefaultValidator, name=wx.ButtonNameStr):
wx.lib.buttons.ThemedGenButton.__init__(self,parent,id,label,pos,size,style,validator,name)
def AcceptsFocusFromKeyboard(self):
return False # does not accept focus
For more complex navigation rules or controls, you could handle wx.EVT_NAVIGATION_KEY and manage the navigation yourself. To get the list of windows to navigate, you can use self.GetChildren(). The index of the the currently focused window in the wx.WindowList can be obtained through .index(mywindow).
With that information, you can navigate through the list whenever the user presses the "navigation key" and set the focus to the next applicable control, skipping those that you don't want to focus.
To make navigating through the list easier, you could create a generator:
def CycleList(thelist,index,forward):
for unused in range(len(thelist)): # cycle through the list ONCE
if forward:
index = index+1 if index+1 < len(thelist) else 0
else:
index = index-1 if index-1 >= 0 else len(thelist)-1
yield thelist[index]
In the dialog, handle wx.EVT_NAVIGATION_KEY:
self.Bind(wx.EVT_NAVIGATION_KEY, self.OnNavigationKey)
def OnNavigationKey(self,event):
children = self.GetChildren() # list of child windows
focused = self.FindFocus() # current focus
# avoid accessing elements that do not exist
if not focused or focused not in children:
event.Skip() # use default behavior
return
index = children.index(focused)
for child in CycleList(children,index,event.GetDirection()):
# default behavior:
if child.AcceptsFocusFromKeyboard():
child.SetFocus()
return
The example above emulates the default behavior: it cycles through focusable controls (skipping unfocusable controls like static texts).
You could expand the check to exclude specific controls or create a custom button class that implements AcceptsFocusFromKeyboard returning False.
NOTE: While wx.PyWindow, wx.PyPanel and wx.PyControl implement the mechanism to allow overriding of AcceptsFocusFromKeyboard, the standard wxPython controls do not.
However, handling wx.EVT_NAVIGATION_KEY and checking AcceptsFocusFromKeyboard on the python side will access the actual python object which will invoke the overridden method.
If you were using C++ this problem has a straightforward solution as described in the remainder of this answer. In wxPython it seems you cannot specialize wxWidgets classes - which seems to me a fatal snag.
You could create a specialization of the button control which will be left out of the tab traversal by overriding AcceptsFocusFromKeyboard() to return FALSE.
http://docs.wxwidgets.org/trunk/classwx_window.html#a2370bdd3ab08e7ef3c7555c6aa8301b8
The following C++ code works fine: focus jumps from the first to the third button when tab is pressed
class cNoTabButton : public wxButton
{
public:
cNoTabButton(wxWindow *parent,
wxWindowID id,
const wxString& label = wxEmptyString,
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = 0 )
: wxButton(parent,id,label,pos,size,style)
{}
bool AcceptsFocusFromKeyboard() const {
return false;
}
};
MyFrame::MyFrame(const wxString& title)
: wxFrame(NULL, wxID_ANY, title)
{
// set the frame icon
SetIcon(wxICON(sample));
wxPanel * panel = new wxPanel(this,-1,wxPoint(0,0),wxSize(500,500));
new wxButton(panel,-1,"TabPlease",wxPoint(20,20));
new cNoTabButton(panel,-1,"NoTabThanks",wxPoint(100,20));
new wxButton(panel,-1,"OKWithMe",wxPoint(200,20));
}
This is not a perfect solution but one way to do this is actually to delay your control's getting the focus back by half a second.
Your main window will get focus back and buttons still work. I use it because I want my main windows to handle all key presses but still to contain buttons on it that are used with the mouse.
So you bind your KILL_FOCUS event in the control that is supposed to preserve the focus and you create a list of controls that cannot get the focus from it.
First a helper function to get all children:
def GetAllChildren(control):
children = []
for c in control.GetChildren():
children.append(c)
children += GetAllChildren(c)
return children
In my case I want all the children of the window to not get focus
self.not_allowed_focus = GetAllChildren(self)
self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
In my KILL FOCUS handler I ask for the focus back after half a second
def OnKillFocus(self,evt):
print "OnKillFocus"
win = evt.GetWindow()
if win in self.not_allowed_focus:
wx.CallLater(500,self.SetFocus)

Categories

Resources