GTK4 ApplicationWindow that is also a ScrolledWindow? - gtk

Here is a minimal GTK 4 app written using the Python bindings, based on this tutorial. It displays a window with a very long block of text:
import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
class MainWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
long_label = Gtk.Label.new("Here is a very long chunk of text.\n" * 100)
self.set_child(long_label)
class MyApp(Adw.Application):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.connect('activate', self.on_activate)
def on_activate(self, app):
self.win = MainWindow(application=app)
self.win.present()
if __name__ == "__main__":
app = MyApp(application_id="com.example.GtkApplication")
app.run(sys.argv)
The tutorial uses the class Gtk.ApplicationWindow for the MainWindow, which lets you set the application_id in the main function and other convenient features.
However, the default Gtk.ApplicationWindow does not include a scrollbar. Rather than build one manually, it seems the best way to do this is to make my main window an instance of Gtk.ScrolledWindow, documented here, which lets you specify the scroll behavior etc. at a high level.
Unfortunately, dropping Gtk.ScrolledWindow in place of Gtk.ApplicationWindow causes an error:
TypeError: gobject `__main__+MainWindow' doesn't support property `application'
Is there a way to create a Gtk.ApplicationWindow that is also a Gtk.ScrolledWindow?
If not, what is the best way to get a scrollbar in my main window?'
(I have added the pygobject tag because it is the language of my MWE, but I think that my question is agnostic with respect to the binding language.)

A Gtk.ScrolledWindow isn't really a "window" per se as most people know it (it used to map to the very specific concept of a X11 Window). It's a widget that allows you to scroll through its child widget.
In other words, if you want an application window where you can scroll through the content, you would wrap your Gtk.Label with a Gtk.ScrolledWindow and you would then set that as the Gtk.ApplicationWindow's child.
In other words, your MainWindow would become:
class MainWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
long_label = Gtk.Label(label=("Here is a very long chunk of text.\n" * 100))
# Wrap a scrolled window around it
scrolled = Gtk.ScrolledWindow()
scrolled.set_child(long_label)
# Set it as the main child of the window
self.set_child(scrolled)

Related

Adding checkable combobox in QGIS plugin builder plugin

I am developing a QGIS plugin in python and hit a roadblock when displaying my GUI. I am using the plugin builder framework to develop my plugin and I have trouble displaying a checkable combo box in a scrollArea in my GUI. The code with core functionality is as follows.
def run(self):
# Only create GUI ONCE in callback, so that it will only load when the plugin is started
if self.first_start == True:
self.first_start = False
# Sets up the pyqt user interface
self.dlg = EarthingToolDialog()
# Fetching the active layer in the QGIS project
layer = self.iface.activeLayer()
checkable_combo = CheckableComboBox()
# Going through each field of the layer
# and adding field names as items to the
# combo box
for j,field in enumerate(layer.fields()):
checkable_combo.addItem(str(field.name()))
# Setting the checked state to True by default
checkable_combo.setItemChecked(j, True)
# putting the check box inside the scroll area of the GUI
self.dlg.scrollArea.setWidget(checkable_combo)
self.dlg.scrollArea.setMinimumSize(QSize(700,400))
# show the dialog
self.dlg.show()
# Run the dialog event loop
self.dlg.exec_()
class EarthingToolDialog(QtWidgets.QDialog, FORM_CLASS):
def __init__(self, parent=None):
"""Constructor."""
super(EarthingToolDialog, self).__init__(parent)
self.setupUi(self)
class CheckableComboBox(QComboBox):
def __init__(self):
super().__init__()
self._changed = False
self.view().pressed.connect(self.handleItemPressed)
def setItemChecked(self, index, checked=False):
print('checked')
item = self.model().item(index, self.modelColumn()) # QStandardItem object
print(type(item))
if checked:
item.setCheckState(Qt.CheckState.Checked)
else:
item.setCheckState(Qt.CheckState.Unchecked)
def handleItemPressed(self, index):
print('pressed')
item = self.model().itemFromIndex(index)
if item.checkState() == Qt.Checked:
item.setCheckState(Qt.Unchecked)
else:
item.setCheckState(Qt.Checked)
self._changed = True
print('set ' + str(item.checkState()))
def hidePopup(self):
print('hide')
if not self._changed:
super().hidePopup()
self._changed = False
def itemChecked(self, index):
print('read')
item = self.model().item(index, self.modelColumn())
return item.checkState() == Qt.Checked
In summary, the run function is the main function called by the plugin when it is loaded. self.dlg is the instance of the actual pyqt python user interface. This is rendered with the help of the EarthingToolDialog class. The checkable combo box and it's functionalities are self contained in the CheckableComboBox class.
The run function executes without any error when the plugin is loaded but the checkboxes are not visible in the combobox. Just a normal combo box with a list of items (just the standard dropdown combo box) is seen on the GUI's scroll area and not the desired checkable combo box. The CheckableComboBox class was taken from https://morioh.com/p/d1e70112347c and it runs perfectly well in the demo code shown there.
I understand that this is a very specific question and it would be great if someone could figure out what the problem might be. Thanks in advance!
Within the run function, this piece of codes didn't work for me:
self.dlg.scrollArea.setWidget(checkable_combo)
self.dlg.scrollArea.setMinimumSize(QSize(700,400))
So instead, I use:
layout = QVBoxLayout()
layout.addWidget(checkable_combo)
self.dlg.setLayout(layout)
I didn't use directly this class (It was generated automatically since I use Plugin Builder, so in here I commented it):
class EarthingToolDialog(QtWidgets.QDialog, FORM_CLASS):
def __init__(self, parent=None):
"""Constructor."""
super(EarthingToolDialog, self).__init__(parent)
self.setupUi(self)
Now, in order to display checkable combo box, CheckableComboBox constructor is changed as :
def __init__(self):
super().__init__()
self._changed = False
self.view().pressed.connect(self.handleItemPressed)
delegate = QtWidgets.QStyledItemDelegate(self.view())
self.view().setItemDelegate(delegate)
The last two lines are from the answer listed here.
Codes display checkable combo box list with all items checked by default.

Organizing kivy layout(s) and classes

I have a rather large App split in a single .kv file and a GUI.py file accessing a library of other .py files. I came to a point where i want to organize everything and split up large layouts into different classes and .kv files.
Currently im working on a function which should add and remove a certain layout to my main layout while still accessing variables and functions of the base class (called BoxL). I tried various things, but i dont know how i can hand over/instantiate my main class to/in my new class.
Im trying to build a rough minimal example:
Main python file: GUI.py
import kivy
from kivy.app import App
from kivy.config import Config
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
class AdvancedClass(BoxLayout):
"""This is my new class from which i want to access BoxL class."""
pass
class BoxL(BoxLayout):
def __init__(self):
super(BoxL, self).__init__()
some_variable = 1
advanced_mode_enabled = False
def some_function(self):
print(self.some_variable)
print('Im also doing stuff in GUI.kv file with ids')
self.ids.test_label.text = '[b]some text with markup'
def advanced_mode(self):
if not self.advanced_mode_enabled:
print('Enabling advanced mode ..')
self.ids.master_box.add_widget(self.advanced_class_obj)
self.advanced_mode_enabled = True
else:
print('Disabling advanced mode ..')
self.ids.master_box.remove_widget(self.advanced_class_obj)
self.advanced_mode_enabled = False
class GUI(App):
def build(self):
Builder.load_file('layouts.kv') # i read its best to instanciate kivy files here once everything is loaded
BoxL.advanced_class_obj = AdvancedClass()
if __name__ == "__main__":
GUI().run()
Main layout file: GUI.kv
<BoxL>:
orientation: 'vertical'
id: master_box
Label:
text: str(root.some_variable)
Button:
text: 'Change variable'
on_release:
root.some_variable = 2
Button:
text: 'Add advanced layout'
on_release:
root.advanced_mode(self)
New layout file layouts.kv from which i want to access functions/variables of BoxL class in GUI.py:
<AdvancedClass>:
orientation: 'vertical'
Label:
text: '[b]TEST'
TextInput:
hint_text: 'TEST'
Button:
text: 'KLICK'
on_release:
# print(parent.some_variable)
# print(self.some_variable)
# print(BoxL.some_variable)
print('Im not sure how to do this .. ') # issue-point
I hope this covers everything. Im struggling with this for quite a while.
Figured it out: app.root.FUNCTION()

is there a way to take numerical inputs in gtk+, pygobject?

I need to implement a input dialog box that takes in numerical values. How is it done in Pygobject?. It is similar to excel taking numerical inputs.
If you're looking for simple numerical values, you can use Gtk.SpinButton.
The more general way getting input from the user in the UI is Gtk.Entry. That widget does not support built-in validation, but it can be trivially implemented, similarly as shown in this stackoverflow answer (by overriding the insert_text method).
If you want to show validation errors to your user, you can use either CSS styling, or for example set the secondary icon (with e.g. the secondary_icon_name property) to a warning sign, with an accompanying label explaining what is wrong.
With some help from Ricardo Silva Veloso, this is basically the code I was suggesting to write in my first comment (also considering #nielsdg secondary icon suggestion):
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
class MyEntry(Gtk.Entry, Gtk.Editable):
__gtype_name__ = 'MyEntry'
def __init__(self, **kwargs):
super(MyEntry, self).__init__(**kwargs)
def do_insert_text(self, text, length, position):
if text.isnumeric():
self.props.secondary_icon_name = None
self.get_buffer().insert_text(position, text, length)
return length + position
else:
self.props.secondary_icon_name = "dialog-error"
self.props.secondary_icon_tooltip_text = "Only numbers are allowed"
return position
win = Gtk.Window()
myentry = MyEntry()
win.add(myentry)
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

pygtk menubar with fixed

I'm new to pyGTK, and now I'm trying to create a menubar with a fixed layout, but I only get a background on the items, not on the entire bar. My code:
import gtk
class App(gtk.Window):
def __init__(self):
super(App,self).__init__()
self.set_size_request(640,480)
self.set_position(gtk.WIN_POS_CENTER)
menubar = gtk.MenuBar()
menu_file= gtk.Menu()
menuitem_file = gtk.MenuItem("File")
menuitem_file.set_submenu(menu_file)
menuitem_exit = gtk.MenuItem("Exit")
menuitem_exit.connect("activate",gtk.main_quit)
menu_file.append(menuitem_exit)
menubar.append(menuitem_file)
fixed = gtk.Fixed()
vbox = gtk.VBox(False, 2)
vbox.pack_start(menubar, False, False, 0)
fixed.add(vbox)
self.add(fixed)
self.connect("destroy",gtk.main_quit)
self.show_all()
App ()
gtk.main ()
You need to make vbox request size, e.g. add vbox.set_size_request (300,50) and see the difference. It is not correct size, but then I don't know why you use gtk.Fixed at all. In 99.95% of case you don't need gtk.Fixed. And especially if you are new to GTK+ you might think you need it while you actually don't.

Receiving keystrokes in an opened wx.ComboCtrl

Coming from this question, I have a wxComboCtrl with a custom popup made of a panel with a bunch of radiobuttons.. My problem is that when I open the popup the combo doesn't get keystrokes, because the events get handled by the panel itself.. I'd like to redirect those KeyEvents to the textctrl of the combo, but I can't find a way to get it to work :/
Am I going the wrong way? Should I manually handle the textctrl value as the user presses keys? I think that would be a bit cumbersome though.. Since supposedly the textctrl already knows how to handle those events..
Here's my testcase (wxPython 2.8 on Linux), the "on_key" method should be the culprit:
import wx
import wx.combo
class CustomPopup(wx.combo.ComboPopup):
def Create(self, parent):
# Create the popup with a bunch of radiobuttons
self.panel = wx.Panel(parent)
sizer = wx.GridSizer(cols=2)
for x in range(10):
r = wx.RadioButton(self.panel, label="Element "+str(x))
r.Bind(wx.EVT_RADIOBUTTON, self.on_selection)
sizer.Add(r)
self.panel.SetSizer(sizer)
# Handle keyevents
self.panel.Bind(wx.EVT_KEY_UP, self.on_key)
def GetControl(self):
return self.panel
def GetAdjustedSize(self, minWidth, prefHeight, maxHeight):
return wx.Size(200, 150)
def on_key(self, evt):
if evt.GetEventObject() is self.panel:
# Trying to redirect the key event to the combo.. But this always returns false :(
print self.GetCombo().GetTextCtrl().GetEventHandler().ProcessEvent(evt)
evt.Skip()
def on_selection(self, evt):
self.Dismiss()
wx.MessageBox("Selection made")
class CustomFrame(wx.Frame):
def __init__(self):
# Toolbar-shaped frame with a ComboCtrl
wx.Frame.__init__(self, None, -1, "Test", size=(800,50))
combo = wx.combo.ComboCtrl(self)
popup = CustomPopup()
combo.SetPopupControl(popup)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(combo, 0)
self.SetSizer(sizer)
self.Layout()
if __name__ == '__main__':
app = wx.PySimpleApp()
CustomFrame().Show()
app.MainLoop()
Edit:
I found these (unresolved) discussions on the same topic..
"ComboCtrl loses keyboard focus when ComboPopup is shown "
"issue using wx.ComboCtrl"
I had the same problem with a custom ComboPopup control using a panel as the basis of the control.
EmulateKeypress just entered a never-ending loop. I found binding the panel to the EVT_KEY_DOWN and EVT_CHAR to the overridden OnComboKeyEvent function and skipping the event down the event handling chain was all that was required to enable entering text into the textbox of the ComboCtrl with the popup open. I think this makes sense. As the Popup isn't strictly a control its events won't belong anywhere until bound into the the wx.app event loop through the Bind() method.
I've included the overridden OnComboKeyEvent function below so that anyone else can explore this problem.
def create(self, *args, **kwds):
...
self.panel.Bind(wx.EVT_CHAR, self.OnComboKeyEvent)
self.panel.Bind(wx.EVT_KEY_DOWN, self.OnComboKeyEvent)
...
def OnComboKeyEvent(self, evt):
evt_dict = {wx.EVT_TEXT.typeId: "TextEvent",
wx.EVT_CHAR.typeId:"CharEvent",
wx.EVT_CHAR_HOOK.typeId:"CharHookEvent",
wx.EVT_KEY_UP.typeId:"KeyUpEvent",
wx.EVT_KEY_DOWN.typeId:"KeyDownEvent"}
if evt.GetEventType() in evt_dict:
thelogger.debug("oncombokeyevet:%s" % evt_dict[evt.GetEventType()])
super().OnComboKeyEvent(evt)
evt.Skip()
I've got an answer from Robin Dunn himself on wxpython-users:
Sending low-level events to native
widgets like this does not work the
way that most people expect it to.
You are expecting this to result in
the character being added to the text
ctrl,but it can't do that because all
that ProcessEvent does is send the
wx.Event to any bound event handlers
and then it stops. It does not go to
the next step and convert that event
to the equivalent native message and
send it on to the native widget. See
the "Simulating keyboard events"
thread.
It doesn't work especially well on
non-Windows platforms, but you could
try calling EmulateKeypress instead.