Inline renaming of a tab in QTabBar/QTabWidget - c++

I really want to have a QTabWidget that can be renamed by double-click on tab caption.
I have googled and found this solution, but it's more like an outline for a developer who knows his way through Qt and subclassing it's widgets.
I'm kind of stuck how to implement all that. I have used an example given further down that thread (with IndependentLineEdit and so on) and it worked, but it's not what i wanted.
I do not want to have any kind of InputDialog. I do not want to have any floating widget or something.
What i basically want is to use QTabWidget (subclass), that behaves the similar way as spreadsheet tabs do in modern office suites - like, tab label is substituted with seamless line edit, which resets tab label on Enter or leaves it intact on Esc.
I have not been able to find such a solution so far. I do understand that what I actually need is very close to this:
provide a temporary QLineEdit at QTabBar::tabRect() filled with QTabBar::tabText()
but i do not understand how to do that. Moreover, since QTabBar is kind of bare tabbar i would also prefer to have in enclosed into QTabWidget (subclass).

The following is an implementation of such behavior in Python in PyQt4. It should be easy to convert that to C++. Please note that Qt5 has a nice signal tabBarDoubleClicked which is missing in Qt4. This piece of code creates such signal as well.
from PyQt4 import QtGui
from PyQt4.QtCore import pyqtSignal, pyqtSlot
class QTabBar(QtGui.QTabBar):
"""QTabBar with double click signal and tab rename behavior."""
def __init__(self, parent=None):
super().__init__(parent)
tabDoubleClicked = pyqtSignal(int)
def mouseDoubleClickEvent(self, event):
tab_index = self.tabAt(event.pos())
self.tabDoubleClicked.emit(tab_index)
self.start_rename(tab_index)
def start_rename(self, tab_index):
self.__edited_tab = tab_index
rect = self.tabRect(tab_index)
top_margin = 3
left_margin = 6
self.__edit = QtGui.QLineEdit(self)
self.__edit.show()
self.__edit.move(rect.left() + left_margin, rect.top() + top_margin)
self.__edit.resize(rect.width() - 2 * left_margin, rect.height() - 2 * top_margin)
self.__edit.setText(self.tabText(tab_index))
self.__edit.selectAll()
self.__edit.setFocus()
self.__edit.editingFinished.connect(self.finish_rename)
#pyqtSlot()
def finish_rename(self):
self.setTabText(self.__edited_tab, self.__edit.text())
self.__edit.deleteLater()
class QTabWidget(QtGui.QTabWidget):
"""QTabWidget with double click on tab signal and tab rename behavior."""
def __init__(self, parent):
super().__init__(parent)
self.setTabBar(QTabBar())
self.tabBar().tabDoubleClicked.connect(self.tabBarDoubleClicked)
tabBarDoubleClicked = pyqtSignal(int)
Please note that there is a rather hacky solution to aligning the qlineedit within the tab header. It is achieved by fixing the margin to a certain value. I'm sure it is possible to look that value up in the style if necessary.

Related

PyQt5 QComboBox list items changing postion

I am facing some issue with the display style of Qcombobox items. Currently am hardcoding the data to be shown in the combobox.
here is the code :
self.Dummy = QComboBox(self)
self.Dummy.setGeometry(200,600, 350, 50)
self.Dummy.setStyleSheet("QComboBox {background-color: white;border-style: outset;" border-width: 2px;border-radius: 5px;border-color: #448aff; font: 12px; min-width: 10em; padding: 3px;}")
self.Dummy.addItems(["-Select-", "2", "3","4","5","6","7","8","9","0","11",])
The issue is that the dropdown "list" postion keeps changing after each selection. Here is the image of the issue am facing.
Below is my combobox
The list contains items <-Select->,2,3,4,5,6,7,8,9,0,11 , where <-Select-> will be the first element shown.
Now when I click the box, the box list "down" the elements and suppose I selected '2'. Then, if I try to select another item, the list will be dropped in a "downwards" direction. see below
Now, say if selected the last element from the items, '11'. Now if I try to select a new item by clicking on the box, the list will be popped "up" instead of down. see below
What should be done to fix it ? I don't think its an issue with stylesheet, without it also, this issue is happening. The reason I need this to be fixed is that when the list is popping up, its covering the label above it
What you see is a behavior that is OS and style dependent.
To avoid it, the best way is to subclass QComboBox and overwrite showPopup(), then we call the base class implementation (which is responsible of showing, resizing and positioning the popup view) and move it if necessary.
class Combo(QtWidgets.QComboBox):
def showPopup(self):
super().showPopup()
# find the widget that contains the list; note that this is *not* the view
# that QComboBox.view() returns, but what is used to show it.
popup = self.view().window()
rect = popup.geometry()
if not rect.contains(self.mapToGlobal(self.rect().center())):
# the popup is not over the combo, there's no need to move it
return
# move the popup at the bottom left of the combo
rect.moveTopLeft(self.mapToGlobal(self.rect().bottomLeft()))
# ensure that the popup is always inside the edges of the screen
# we use the center of the popup as a reference, since with multiple
# screens the combo might be between two screens, but that position
# could also be completely outside the screen, so the cursor position
# is used as a fallback to decide on what screen we'll show it
done = False
for i, pos in enumerate((rect.center(), QtGui.QCursor.pos())):
for screen in QtWidgets.QApplication.screens():
if pos in screen.geometry():
screen = screen.geometry()
if rect.x() < screen.x():
rect.moveLeft(screen.x())
elif rect.right() > screen.right():
rect.moveRight(screen.right())
if rect.y() < screen.y():
rect.moveTop(screen.y())
elif rect.bottom() > screen.bottom():
# if the popup goes below the screen, move its bottom
# *over* the combo, so that the its current selected
# item will always be visible
rect.moveBottom(self.mapToGlobal(QtCore.QPoint()).y())
done = True
break
if done:
break
popup.move(rect.topLeft())
This can also be done without subclassing (for example if you have many combos, you created the UI from Designer and don't want to use promoted widgets), but you'll have to remember to change all referencies to the combo.
class MyWindow(QtWidgets.QWidget):
def __init__(self):
# ...
self.combo = QtWidgets.QComboBox()
self.combo.showPopup = self.showPopupAndCheck
def showPopupAndCheck(self):
QtWidgets.QComboBox.showPopup(self.combo)
popup = self.view().window()
rect = popup.geometry()
if not rect.contains(self.combo.mapToGlobal(self.combo.rect().center())):
# the popup is not over the combo, there's no need to move it
return
# change everything from self to self.combo
Alternatively, if you want to keep this behavior consistent through all your program without always using the subclass, you can use some sort of monkey patching hack.
The advantage is that any QComboBox you create (even when loading UI files or creating a combo at runtime) will always use the new behavior.
Important: this MUST be at the very beginning of the main file of your program, possibly just after the import section.
from PyQt5 import QtCore, QtGui, QtWidgets
def customShowPopup(self):
# we can't use super(), because we're not in the class definition, but
# calling the class method with "self" as first argument is practically the
# same thing; note the underscore!
QtWidgets.QComboBox._showPopup(self)
popup = self.view().window()
# ... go on, exactly as above
# create a new reference to the showPopup method, which is the one we've used
# in the function above
QtWidgets.QComboBox._showPopup = QtWidgets.QComboBox.showPopup
# overwrite the original reference with the new function
QtWidgets.QComboBox.showPopup = customShowPopup

Correct parameter to pass a bound event to treeview.identify/identify_region in Tkinter?

I have a couple of treeviews tkinter widgets. For style purposes i need to prevent a manual resizing of the treeview columns width without totally disableing column resize.
Using a possible solution provided here: How to disable manual resizing of Tkinter's Treeview column? i am catching single click events on separators between the columns and stop the event there. For this purpose every treeview binds a handling function.
Example:
self.tree.bind('<Button-1>', lambda event: handle_treeview_single_click(self.tree, event))
def handle_treeview_single_click(tree, event):
if tree.identify_region(event.x, event.y) == 'separator':
return 'break'
Running the code produce the following error:
File "C:\Program Files (x86)\Python27\ArcGIS10.2\lib\lib-tk\ttk.py", line 1277, in identify
return self.tk.call(self._w, "identify", component, x, y)
TclError: bad component "region": must be row or column
this doesn't change regardless the object i click on (Header, cell or separator). For example the relating parameter look like this:
tree = Treeview: .42424440.47830640.47831440.47831800.47831840
x = 464
y = 14
Not sure what im missing here, even more since im fairly new to python.
There is a comment in the ttk source code that explains why this might not be working:
def identify_region(self, x, y):
"""
...
* Availability: Tk 8.6"""
return self.identify("region", x, y)
Since you're using python 2.7, you probably aren't using tk 8.6. You can verify this by printing out the value of Tkinter.TkVersion

How to enable autoscroll in Qtableview like in Qtextedit?

I'm using QtableView and QStandardItemModel to display logs on GUI to maintain proper spacing and filter logs. I created model and inserted data into it. Used QSortFilterProxyModel for filter strings.
self.tableView = QtGui.QTableView(self)
self.model = QtGui.QStandardItemModel(self)
self.proxy = QtGui.QSortFilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.tableView.setModel(self.proxy)
In a sec, nearly 100 logs are expected and should be shown on GUI. When new logs are appended, the view isn't auto scrolling and the slider stays only at the top. It doesn't give live feel for logging and user need to scroll manually to the end. So to overcome this, i used following syntax,
self.model.rowsInserted.connect(lambda: QtCore.QTimer.singleShot(5, self.tableView.scrollToBottom))
It gives live feel for logs, but the slider remains always in bottom and i'm not able to scroll up to see previous logs. Whenever i try to move the slider, it immediately comes down to bottom again. So this syntax doesn't meet my requirement. In QTextEdit, auto scrolling is proper and user friendly. I want the same scenario here on QtableView. Is there any alternative for auto scrolling which resembles like QTextEdit ?
To get the required behaviour, you can auto-scroll only when the previous scroll position is at the bottom. That way, whenever the user scrolls away from the bottom, auto-scrolling will be disabled; but when they scroll back to the bottom, auto-scrolling will be re-enabled. (NB: to quickly re-enable auto-scroll, right-click the scrollbar and select "Bottom" from the context menu).
Here is a simple demo:
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.table = QtGui.QTableView(self)
self.model = QtGui.QStandardItemModel(self)
self.table.setModel(self.model)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.table)
self._scroll = True
self.model.rowsAboutToBeInserted.connect(self.beforeInsert)
self.model.rowsInserted.connect(self.afterInsert)
def beforeInsert(self):
vbar = self.table.verticalScrollBar()
self._scroll = vbar.value() == vbar.maximum()
def afterInsert(self):
if self._scroll:
self.table.scrollToBottom()
def addRow(self):
self.model.appendRow([QtGui.QStandardItem(c) for c in 'ABC'])
if __name__ == '__main__':
app = QtGui.QApplication([''])
window = Window()
window.setGeometry(500, 50, 400, 300)
window.show()
timer = QtCore.QTimer()
timer.timeout.connect(window.addRow)
timer.start(200)
app.exec_()

wxpython scrolledwindow not displaying

Relatively new to python (2.7) and trying to figure out wxpython (so I apologise in advance for any poor use of code). I've got a GUI of which I have multiple switchable panels on a frame. I need the frame to be scrollable, so I've used ScrolledWindow but now some of the of the GUI elements which are below the initial frame size do not show upon scrolling on.
I've found that changing my monitor resolution solves the problem, but I want to be able to have this working regardless of resolution.
Below is an example of the problem I'm having (doesn't display hi4 and cuts off hi4)
import wx
from apanel import apanel
class simpleapp_wx(wx.Frame):
def __init__(self,parent,id,title):
wx.Frame.__init__(self,parent,id,title,size=(1000,1100))
self.parent=parent
self.scroll = wx.ScrolledWindow(self, -1)
self.scroll.SetScrollbars(1,1,1000,1100)
button0=wx.Button(self.scroll,-1,"hi0",(100,610))
self.panel=apanel(self.scroll)
self.CreateStatusBar()
self.sizer= wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.panel, 1, wx.EXPAND)
self.SetSizer(self.sizer)
self.Show(True)
app=wx.App(False)
frame=simpleapp_wx(None,-1,'Demo')
frame.Show()
app.MainLoop()
and panel is in another class (in a seperate file I called apanel.py)
import wx
class apanel(wx.Panel):
def __init__(self,parent):
wx.Panel.__init__(self,parent=parent)
button=wx.Button(self,-1,"hi",(800,60))
button2=wx.Button(self,-1,"hi2",(200,600))
button3=wx.Button(self,-1,"hi3",(800,800))
button4=wx.Button(self,-1,"hi4",(500,900))
button5=wx.Button(self,-1,'hi5',(10,100))
I've found some errors in your code, it's simple to solve. Look the working panel bellow:
class simpleapp_wx(wx.Frame):
def __init__(self,parent,id,title):
wx.Frame.__init__(self,parent,id,title,size=(1000,1100))
self.parent=parent
self.scroll = wx.ScrolledWindow(self, -1)
self.scroll.SetScrollbars(1,1,1000,1100)
self.CreateStatusBar()
sizer = wx.BoxSizer(wx.VERTICAL)
self.scroll.SetSizer(sizer) # The scrolledWindow sizer
self.panel = wx.Panel(self.scroll)
sizer.Add(self.panel, 0, wx.EXPAND)
button0=wx.Button(self.panel,-1,"hi0",(100,610))
Remarks:
If you use a scrolled window, create a sizer, and set the sizer in scrolled window.
The panel apanel need to be added on scrolled sizer created in line above.
The panel not resizing because simpleapp_wx (Frame) was set your size by the created BoxSizer, the order is inverse.
If you add some button after, put the apanel with parent, not scrolledwindow.
I suggest to you to use wxPython demo and docs: http://www.wxpython.org/download.php have a bunch of working examples.
Good luck in your wxpython studies!

Qt. QSplitter. Handle double click when cursor is under splitter

I need to handle double click for QSplitter when cursor is under splitter.
I redefined mouseDoubleClickEvent. But this does not work for this case.
When I do doulble click when cursor is under splitter (ready to move splitter) the method is not calling.
You can use an event filter to filter all events going to the handle of Qsplitter :
bool MyClass::eventFilter(QObject * obj, QEvent * event)
{
if(event->type()==QEvent::MouseButtonDblClick)
{
...
}
return false;
}
Also don't forget to install event filter in the constructor of your class :
MyClass::MyClass(QWidget *parent):QWidget(parent)
{
...
ui->splitter->handle(1)->installEventFilter(this);
...
}
I needed the same in order to be able to evenly space the widgets in the splitter when the user double clicks on the handle (this was my use case). Overriding QSplitter.mouseDoubleClickEvent() does not work because it seems that the handle consumes the double click event itself so it is not propagated to the parent QSplitter. The solution proposed in the accepted answer using eventFilter is quite good but is has the disadvantage that it is not 'dynamic', i.e. the event filter is not installed when the user adds new widgets to the splitter during runtime. So we need to find a way to install the event filter dynamically. There are two options to achieve this:
Override QSplitter.addWidget() and QSplitter.insertWidget():
# inside class MySplitter
def addWidget(self, widget):
super(MySplitter, self).addWidget(widget) # call the base class
self.handle(self.count() - 1).installEventFilter(self)
def insertWidget(self, index, widget):
super(MySplitter, self).insertWidget(index, widget) # call the base class
self.handle(index).installEventFilter(self)
but this is a bit problematic when the user adds widgets by not using these two methods but by setting parent to the child widget, though this is discouraged by the docs - see: http://doc.qt.io/qt-5/qsplitter.html#childEvent
Intercept the childEvent(), which feels a bit hacky but is error proof:
# inside class MySplitter
def childEvent(self, event):
if event.added():
# Note that we cannot test isinstance(event.child(), QSplitterHandle)
# because at this moment the child is of type QWidget,
# it is not fully constructed yet.
# So we assume (hacky but it works) that the splitter
# handle is added always as the second child after
# the true child widget.
if len(self.children()) % 2 == 0:
event.child().installEventFilter(self)
super(MySplitter, self).childEvent(event) # call the base class
I am using b) and it works for me quite well. This has the advantage that you do not need to subclass (I do, however, for teh sake of simplicity), you can install another event filter to intercept the childEvent and install the event filter from outside.
Sorry my code is in PyQt, but I think it is idiomatic enough and easily translated to C++.