Python and GTK+3: widget for choosing a keyboard shortcut - python-3.x

I'm looking for a way to add a shortcut chooser widget on a dialog with Python and GTK+3.
I tried to search through all available widgets and don't seem to find any out-of-the-box solution. What would be my best call in that respect? Should I use a GtkEntry and intercept a key press?
Even though it seems like a pretty common use case, I failed to find any working example of that.

There is no out-of-the-box solution, but you can probably find an example to adapt in the Keyboard panel of GNOME Control Center.

I have implemented this myself using a separate dialog. There's a regular button displaying the current assignment, which, when clicked, opens a KeyboardShortcutDialog implemented as follows:
class KeyboardShortcutDialog(Gtk.Dialog):
"""Dialog that allows to grab a keyboard shortcut."""
def __init__(self, parent):
Gtk.Dialog.__init__(self, _("Keyboard shortcut"), parent, 0)
self.shortcut = None
self.set_border_width(32)
# Add a label
label = Gtk.Label(xalign=0.5, yalign=0.5)
label.set_markup(
_('Press the desired key combination, <b>Backspace</b> to remove any shortcut, or <b>Esc</b> to cancel.'))
self.get_content_area().pack_start(label, True, True, 0)
self.connect('key-press-event', self.on_key_press)
self.show_all()
def on_key_press(self, widget, event: Gdk.EventKey):
"""Signal handler: key pressed."""
keyval = event.get_keyval()[1]
name = Gdk.keyval_name(keyval)
# For some reason event.is_modifier == 0 here, even for modifier keys, so we need to resort to checking by name
if name not in [
'Shift_L', 'Shift_R', 'Control_L', 'Control_R', 'Meta_L', 'Meta_R', 'Alt_L', 'Alt_R', 'Super_L',
'Super_R', 'Hyper_L', 'Hyper_R']:
logging.debug('Key pressed: state=%s, keyval=%d', event.state, keyval)
self.shortcut = (
keyval,
event.state &
(Gdk.ModifierType.META_MASK | Gdk.ModifierType.SUPER_MASK | Gdk.ModifierType.HYPER_MASK |
Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK))
self.response(Gtk.ResponseType.ACCEPT)
return True
def run(self):
"""Show the dialog and block until it's closed.
:return: tuple (keyval, state) of the key captured or None if the dialog has been closed."""
super().run()
return self.shortcut
The dialog's run() method returns a tuple specifying the pressed key combination.

Related

Get list of existing QShortcuts

I would like to create various objects with associated keyboard shortcuts. The following code works fine. However, it doesn't prevent multiple objects from being assigned the same shortcut keys.
I'd like the create_shortcut function to check if there are existing shortcuts with the same keys and return an error if there are.
I've looked over the documentation for QWidget but haven't found any way to list existing shortcuts.
Any idea how to do this?
class MyClass(QObject):
def __init__(self, parent, shortcut_keys: str):
super().__init__(parent)
self.create_shortcut(shortcut_keys)
def create_shortcut(self, shortcut_keys) -> None:
parent = self.parent()
self.shortcut = QShortcut(QKeySequence(shortcut_keys), parent)
self.shortcut.activated.connect(self.do_stuff)
def do_stuff(self):
...
QShortcut inherits from QObject, meaning that it is always shown in the list of children of the parent for which it was created.
Assuming that the shortcuts have always been created with a parent, just use findChildren(className):
def create_shortcut(self, shortcut_keys) -> None:
seq = QKeySequence(shortcut_keys)
for other in self.parent().findChildren(QShortcut):
if seq == other.key():
# already exists, ignore
return
# create new shortcut
shortcut = QShortcut(QKeySequence(shortcut_keys), parent)
shortcut.activated.connect(self.do_stuff)
Note: creating persistent instance attributes is pointless whenever those attributes will be most certainly overwritten.
Alternatively, just create a list as instance attribute, append new shortcuts to it and always check for them before creating others.

PyQt5 Make QDialog wait for a process to finish before allowing user input

After searching for some time and finding many results for PyQt4 I was not able to convert myself, I need some help for a status window my application needs.
The window is opened when a process starts and should not allow any further input by the user in the main GUI, also users should not be able to close it until the process is finished and a close button is activated.
I tried this with a QDialog and omitting the frame (still need to catch 'ESC' key) thus far, but I am convinced there is a better solution. My code:
def resultWindow(self):
self.resultBox = QDialog(self)
self.resultBox.setWindowTitle("Please Wait")
self.OkButton = QtWidgets.QPushButton(self.resultBox)
self.OkButton.setText("Ok")
self.OkButton.setEnabled(False)
self.OkButton.clicked.connect(self.OkButton_clkd)
self.resultBox.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool)
self.resultBox.exec_()
def OkButton_clkd(self):
self.resultBox.close()
So, what is the smarter way to do it?
Instead of removing the frame altogether, which also prevents the user from moving or resizing the dialog, you could just remove the close button from the title bar by doing something like
self.resultBox.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.WindowStaysOnTopHint|
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint)
To catch the escape key for the dialog you can install an eventfilter, e.g.
def resultWindow(self):
self.resultBox = QtWidgets.QDialog(self)
self.resultBox.installEventFilter(self)
....
def eventFilter(self, object, event):
if (object == self.resultBox and
event.type() == QtCore.QEvent.KeyPress and
event.key() == Qt.Key_Escape):
return True
return super().eventFilter(object, event)
or you could subclass QDialog and override keyPressEvent()

tkinter GUI design: managing variables from multiple widgets/toolbars

{Edit: the answer by Bryan Oakley in the suggested duplicate question enter link description here a) fires a response on change to the array variable (arrayvar.trace mode="w"), and I need it triggered on FocusOut, as described in my original question; b) works for Python 2, but I'm having trouble converting it to work in Python 3.5. I'm currently using his and pyfunc's answers as leads and trying to figure out a similar solution using a FocusOut event.}
I am working on a tkinter GUI that lets a user select a particular type of calculation, using a pair of radio button lists. Based on the selections, a tool bar is populated with multiple modular entry widgets, one for each variable the calculation requires. The goal is to have the numerical entry values passed to the model, which will return data to be graphed on a canvas or matplotlib widget.
My question is: what typical strategy is used for gathering and continually refreshing values from multiple widgets, in order to update displays and to pass them on to the model? The trick here is that there will be a large number of possible calculation types, each with their own toolbar. I'd like the active toolbar to be "aware" of its contents, and ping the model on every change to a widget entry.
I think the widgets and the toolbar would have to be classes, where the toolbar can query each widget for a fresh copy of its entry values when a change is detected, and store them as some collection that is passed to the model. I'm not entirely sure how it can track changes to the widgets. Using a "validate='focusout' " validation on the entry widget (e.g. as in
this validation reference )
suggests itself, but I already use "validate='key' " to limit all entries to numbers. I don't want to use "validate=all" and piggyback onto it because I don't want to continually ask the model to do a lengthy calculation on every keypress.
I'm new to GUI programming, however, so I may be barking up the wrong tree. I'm sure there must be a standard design pattern to address this, but I haven't found it.
Below is a screenshot of a mockup to illustrate what I want the GUI to do. The Task radiobutton controls which secondary button menu appears below. The selection in the second menu populates the top toolbar with the necessary entry widgets.
The following code does (mostly) what I want. The ToolBar frame objects will store the values from its contained widgets, and call the appropriate model as needed. The VarBox objects are Entry widgets with extra functionality. Hitting Tab or Return refreshes the data stored in the ToolBar dictionary, tells the ToolBar to send data to the model, and shifts focus to the next VarBox widget.
from tkinter import *
# Actual model would be imported. "Dummy" model for testing below.
def dummy_model(dic):
"""
A "dummy" model for testing the ability for a toolbar to ping the model.
Argument:
-dic: a dictionary whose values are numbers.
Result:
-prints the sum of dic's values.
"""
total = 0
for value in dic.values():
total += value
print('The total of the entries is: ', total)
class ToolBar(Frame):
"""
A frame object that contains entry widgets, a dictionary of
their current contents, and a function to call the appropriate model.
"""
def __init__(self, parent=None, **options):
Frame.__init__(self, parent, **options)
self.vars = {}
def call_model(self):
print('Sending to dummy_model: ', self.vars)
dummy_model(self.vars)
class VarBox(Frame):
"""
A customized Frame containing a numerical entry box
Arguments:
-name: Name of the variable; appears above the entry box
-default: default value in entry
"""
def __init__(self, parent=None, name='', default=0.00, **options):
Frame.__init__(self, parent, relief=RIDGE, borderwidth=1, **options)
Label(self, text=name).pack(side=TOP)
self.widgetName = name # will be key in dictionary
# Entries will be limited to numerical
ent = Entry(self, validate='key') # check for number on keypress
ent.pack(side=TOP, fill=X)
self.value = StringVar()
ent.config(textvariable=self.value)
self.value.set(str(default))
ent.bind('<Return>', lambda event: self.to_dict(event))
ent.bind('<FocusOut>', lambda event: self.to_dict(event))
# check on each keypress if new result will be a number
ent['validatecommand'] = (self.register(self.is_number), '%P')
# sound 'bell' if bad keypress
ent['invalidcommand'] = 'bell'
#staticmethod
def is_number(entry):
"""
tests to see if entry is acceptable (either empty, or able to be
converted to a float.)
"""
if not entry:
return True # Empty string: OK if entire entry deleted
try:
float(entry)
return True
except ValueError:
return False
def to_dict(self, event):
"""
On event: Records widget's status to the container's dictionary of
values, fills the entry with 0.00 if it was empty, tells the container
to send data to the model, and shifts focus to the next entry box (after
Return or Tab).
"""
if not self.value.get(): # if entry left blank,
self.value.set(0.00) # fill it with zero
# Add the widget's status to the container's dictionary
self.master.vars[self.widgetName] = float(self.value.get())
self.master.call_model()
event.widget.tk_focusNext().focus()
root = Tk() # create app window
BarParentFrame = ToolBar(root) # holds individual toolbar frames
BarParentFrame.pack(side=TOP)
BarParentFrame.widgetName = 'BarParentFrame'
# Pad out rest of window for visual effect
SpaceFiller = Canvas(root, width=800, height=600, bg='beige')
SpaceFiller.pack(expand=YES, fill=BOTH)
Label(BarParentFrame, text='placeholder').pack(expand=NO, fill=X)
A = VarBox(BarParentFrame, name='A', default=5.00)
A.pack(side=LEFT)
B = VarBox(BarParentFrame, name='B', default=3.00)
B.pack(side=LEFT)
root.mainloop()

tkinter python maximize window

I want to initialize a window as maximized, but I can't find out how to do it. I'm using python 3.3 and Tkinter 8.6 on windows 7. I guess the answer is just here: http://www.tcl.tk/man/tcl/TkCmd/wm.htm#m8
but I have no idea how to input it into my python script
Besides, I need to get the width and height of the window (both as maximised and if the user re-scale it afterwards), but I guess I can just find that out myself.
You can do it by calling
root.state('zoomed')
If you want to set the fullscreen attribute to True, it is as easy as:
root = Tk()
root.attributes('-fullscreen', True)
However, it doesn't show the title bar. If you want to keep it visible, you can resize the Tk element with the geometry() method:
root = Tk()
w, h = root.winfo_screenwidth(), root.winfo_screenheight()
root.geometry("%dx%d+0+0" % (w, h))
With winfo_width() and winfo_height() you can get the width and height or the window, and also you can bind an event handler to the <Configure> event:
def resize(event):
print("New size is: {}x{}".format(event.width, event.height))
root.bind("<Configure>", resize)
To show maximized window with title bar use the 'zoomed' attribute
root = Tk()
root.attributes('-zoomed', True)
I've found this on other website:
import Tkinter
MyRoot = Tkinter.Tk()
MyRoot.state("zoomed")
MyRoot.mainloop()
This solved my problem.
The first approach is to use the root.state('zoomed'), but is not supposed to be universally available. It works on Windows, and on my Ubuntu machine. However, under my Arch machine it doesn't.
The second is to first get the maxsize, and then set geometry manually, like:
m = root.maxsize()
root.geometry('{}x{}+0+0'.format(*m))
This works on most machines, but not on all. For example, under my Arch the maxsize() returns (1425, 870), while the real geometry of maximized window should be (1440, 848). So, you also couldn't rely on it.
And the third, in my opinion the best approach is to use root.wm_attributes('-zoomed', 1). It is universally available and seems to be the safest. On some machines in could zoom only by width or by height, but comparing to previous method, this one would never give you a window partly ouside of the screen.
Finally, if you want a fullscreen, not just zoomed window, use root.wm_attributes('-fullscreen', 1). It provides a native link to window manager's behavior, thus working much better, than playing with overrideredirect and setting geometry by hand (which on some platforms could lead to unmanaged window, which could be closed only by its own interface or killing the process, won't show on the taskbar, etc...)
The most pythonic is" root.wm_state('zoomed'), as mentioned by #J.F.Sebastian
I recently ran into a similar issue where a library I was supporting needed to add Windows 10 as a development target also. Thanks to the information I found here, This is what we're doing now:
class INI_Link:
"""A connector class between a value stored in an ini file, and a value stored elsewhere that can be get and set with two helper functions."""
def __init__(self, getter, setter, varname, inigroup="Settings", inifile=''):
"""Remember that getter is called first to provide the default value.
Then the ini value is read if available, if not the default value is used."""
self._get = getter
self._set = setter
self._save = lambda value :inidb(inifile)[inigroup].__setitem__(varname, getter())
self._load = lambda :inidb(inifile)[inigroup].get(varname, getter())
#first load
self._lastvalue = self._load()
print(self._lastvalue)
self._set(self._lastvalue)
self._callbacks=[]
def trace(self, callback, mode='w'):
"""this only traces for .value.set() not for changes to the underlying value in either location.
if you never touch this again until .commit() at the end of your program, then it will never trigger until then.
call .probe() to force to check for changes without returning anything."""
self.callbacks.append(callback)
def probe(self):
"""check for changes, if there have been any, allert all traces."""
self._monitor(self._get())
def __get__(self):
value = self._get()
self._monitor(value)
return value
def __set__(self, value):
self._set(value)
self._save(value)
self._monitor(value)
def _monitor(value):
"helper to dispatch callbacks"
if value != self._lastvalue:
self._lastvalue = value
for cb in self._callbacks:
try:
cb()
except:
pass
def commit(self):
"""Call this right before getter is no longer useful."""
self._save(self._get())
And then in the main window class's __init__()
self._geometry = INI_Link(self.tkroot.geometry, self.tkroot.geometry, "window_geometry")
try:
#umbuntu and others, not arch
self._zoomed = INI_Link(lambda:self.tkroot.wm_attributes('-zoomed'),
lambda z: self.tkroot.wm_attributes('-zoomed', z)
, "window_zoomed")
except:
#windows and others, not umbuntu
self._zoomed = INI_Link(lambda: self.tkroot.state() == 'zoomed',
lambda z: self.tkroot.state(['normal','zoomed'][z])
, "window_zoomed")
and then when the window is being closed:
#save zoomed state.
self._zoomed.commit()
try:
if self.tkroot.wm_attributes('-zoomed'):
self.tkroot.wm_attributes('-zoomed', False)
self.tkroot.update()
except:
if self.tkroot.state() != 'normal':
self.tkroot.state('normal')
self.tkroot.update()
#save window size in normal state
self._geometry.commit()
With TkAgg as backend this is the only combination that maximized the window without fullscreen:
win_manager = plt.get_current_fig_manager()
win_manager.window.state('zoomed')
win_manager.full_screen_toggle()

'C++ object destroyed' in QComboBox descendant editor in delegate

I have modified combobox to hold colors, using QtColorCombo (http://qt.nokia.com/products/appdev/add-on-products/catalog/4/Widgets/qtcolorcombobox) as howto for the 'more...' button implementation details. It works fine in C++ and in PyQt on linux, but I get 'underlying C++ object was destroyed' when use this control in PyQt on Windows. It seels like the error happens when:
...
# in constructor:
self.activated.connect(self._emitActivatedColor)
...
def _emitActivatedColor(self, index):
if self._colorDialogEnabled and index == self.colorCount():
print '!!!!!!!!! QtGui.QColorDialog.getColor()'
c = QtGui.QColorDialog.getColor() # <----- :( delegate fires 'closeEditor'
print '!!!!!!!!! ' + c.name()
if c.isValid():
self._numUserColors += 1
#at the next line currentColor() tries to access C++ layer and fails
self.addColor(c, self.currentColor().name())
self.setCurrentIndex(index)
...
Maybe console output will help. I've overridden event() in editor and got:
MouseButtonRelease
FocusOut
Leave
Paint
Enter
Leave
FocusIn
!!!!!!!!! QtGui.QColorDialog.getColor()
WindowBlocked
Paint
WindowDeactivate
!!!!!!!!! 'CloseEditor' fires!
Hide
HideToParent
FocUsOut
DeferredDelete
!!!!!!!!! #6e6eff
Can someone explain, why there is such a different behaviour in the different environments, and maybe give a workaround to fix this.
Here is the minimal example:
http://docs.google.com/Doc?docid=0Aa0otNVdbWrrZDdxYnF3NV80Y20yam1nZHM&hl=en
The problem seems to be a fact, that QColorDialog.color() shows modal dialog, that takes focus from combo, which closes immediately after that, then delegate destroys it.. ooops.
So, the workaround to solve such problems is the event interruption:
In the delegate:
def eventFilter(self, editor, event):
if event.type() == QtCore.QEvent.FocusOut and hasattr(editor, 'canFocusOut'):
if not editor.canFocusOut: return False
return QtGui.QItemDelegate.eventFilter(self, editor, event)
In editor we have to introduce the flag self.canFocusOut and set it to true when FocusOut is forbidden. I'm doing this when 'highlited' signal fires on the element, that shows QColorDialog.

Resources