PyQt5: QAction -- QTextEdit.paste not working with QTabWidget?
Trying to learn python, I have started a project to write a tabbed markdown editor. In the code below everything works as expected, I can:
- Add and Remove tabs
- Use the keyboard to add content
- Use the RMB context menu to add content
- Load content from files
However, when I try to set the QAction: QTextEdit.paste I get the following error
Traceback (most recent call last):
File "~/Documents/Python/Mq/bin/Mq-post.py", line 170, in <module>
M = Main()
File "~/Documents/Python/Mq/bin/Mq-post.py", line 40, in __init__
self.initUi()
File "~/Documents/Python/Mq/bin/Mq-post.py", line 47, in initUi
configMainMenu(self)
File "~/Documents/Python/Mq/bin/ConfigMenu.py", line 47, in configMainMenu
Paste.triggered.connect(self.textArea.paste)
AttributeError: 'Main' object has no attribute 'textArea'
I have tried moving the definition for textArea
from addTab
to __init__
, but, while no errors are reported, and the QTextEdit.paste
action works as expected, I cannot create new tabs.
The code is as follows:
Main:
""" Mq editor main window """
import sys
from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QTabWidget,
QVBoxLayout, QFileDialog, QTextEdit)
from PyQt5.QtGui import (QIcon)
from ConfigMenu import (configMainMenu)
from Functions import (trimFileName, trimHome)
class Main(QMainWindow):
""" Main app """
def __init__(self):
super().__init__()
# Define initial window geometry
self.setWindowTitle('Mq Editor')
self.setWindowIcon(QIcon("Quill.png"))
self.left = 0
self.top = 0
self.width = 1024
self.height = 768
self.setGeometry(self.left, self.top, self.width, self.height)
# Setup a TabBar with movable closable tabs
self.tabs = QTabWidget()
self.tabs.tabCloseRequested.connect(self.closeTab)
self.tabs.setTabsClosable(True)
self.tabs.setMovable(True)
# Define the Layout
widget = QWidget(self)
self.setCentralWidget(widget)
self.layout = QVBoxLayout(widget)
# Defined in add tab, See pylint W0201, (disable to get traceback 2)
# self.textArea = None
self.initUi()
# #####################################
def initUi(self):
""" Set up user interface """
# Create a menu bar
configMainMenu(self)
# Adding the tab widget to the layout needs to come after calling
# 'configMainMenu' and 'createToolBar'
self.layout.addWidget(self.tabs)
# Create first tab
self.addTab()
# Open window in maximized size
self.showMaximized()
# #####################################
def addTab(self):
""" Create Tabs """
# Add tab with new editor instance, (this is only place it works)
self.textArea = QTextEdit()
self.textArea.textChanged.connect(self.textChanged)
self.textArea.createStandardContextMenu()
self.tabs.addTab(self.textArea, "Untitled")
# Switch to new tab
index = self.tabs.count() - 1
self.tabs.setCurrentIndex(index)
self.tabs.setTabToolTip(index, "Untitled")
# #####################################
def closeTab(self, index):
""" Close Tab Handler """
self.tabs.removeTab(index)
# #####################################
def getActiveTab(self):
""" Returns active tab 'index' and 'title' """
# Get the active tabs index
index = self.tabs.currentIndex()
# Get the active tabs title
title = self.tabs.tabText(index)
return index, title
# #####################################
def setActiveTabTitle(self, title, toolTip):
""" Sets the active tab 'title' and 'tooltip' """
# Get the active tabs index
index = self.tabs.currentIndex()
# Set the active tabs title
self.tabs.setTabText(index, title)
self.tabs.setTabToolTip(index, toolTip)
# #####################################
def textChanged(self):
""" textChanged handler """
# Get the active tab's details
index, title = self.getActiveTab()
# Check if last char of title is marked as 'Text Changed'
if title[-1] != '*':
# Not marked as 'Text Changed'
self.tabs.setTabText(index, title + '*')
# #####################################
def closeUnusedTabs(self):
""" Tab house-keeping """
tabIndex = self.tabs.count()
while tabIndex > 0:
index = tabIndex - 1
self.tabs.setCurrentIndex(index)
title = self.tabs.tabText(index)
if title == "Untitled":
self.closeTab(index)
tabIndex = tabIndex - 1
# #####################################
def openFile(self):
""" openFile handler """
# Open file dialogue
FileDialogue = QFileDialog.getOpenFileNames
filePath, _ = FileDialogue(None, "Open File", "",
"Markdown Files (*.md);;All Files (*)")
# check 'filePath' is not empty
if filePath:
# Keep the GUI clean
self.closeUnusedTabs()
# Iterate 'filePath' and open files
for i in filePath:
# Having removed unused tabs, we just need to add a new tab for
# each file 'i'
self.addTab()
# Read file 'i' and load into new tab instance of editor
with open(i, "r", encoding="utf8") as f:
fileContents = f.read()
self.textArea.setPlainText(fileContents)
# Get a trimmed path as the 'tab tooltip'
toolTip = trimHome(i)
# Get a trimmed file name to use as the 'tab title'
tabTitle = trimFileName(i)
# Set the new tabs 'title' and 'tootip'
self.setActiveTabTitle(tabTitle, toolTip)
def Paste(self):
" Test paste click handler "
print("Hit Paste")
m = QApplication(sys.argv)
M = Main()
M.show()
sys.exit(m.exec_())
ConfigMenu:
""" Module to create a menu bar """
from PyQt5.QtWidgets import (QAction)
def configMainMenu(self):
""" Creating a menu bar"""
# mainMenu Definition
mainMenu = self.menuBar()
fileMenu = mainMenu.addMenu('File')
editMenu = mainMenu.addMenu('Edit')
# 'New'
New = QAction('New', self)
New.setShortcut('Ctrl+N')
New.setStatusTip('New File')
New.triggered.connect(self.addTab)
fileMenu.addAction(New)
# 'Close'
Close = QAction('Close', self)
Close.setShortcut('Ctrl+W')
Close.setStatusTip('Close File')
Close.triggered.connect(self.closeTab)
fileMenu.addAction(Close)
# 'Open'
Open = QAction('Open', self)
Open.setShortcut('Ctrl+O')
Open.setStatusTip('Open')
Open.triggered.connect(self.openFile)
fileMenu.addAction(Open)
# 'Paste'
Paste = QAction('Paste', self)
Paste.setShortcut('Ctrl+V')
Paste.setStatusTip('Paste text')
# ### The traceback leads here ###
Paste.triggered.connect(self.textArea.paste) # Fails
# Paste.triggered.connect(self.Paste) # For testing
editMenu.addAction(Paste)
Functions:
""" Useful functions """
from pathlib import Path
from os.path import expanduser
def trimFileName(filePath):
""" Returns size limited 'tab title' based on 'file name' """
# FRST: Get the file name without extension
# Convert 'file path' to type 'path' without '.ext'
filePath = Path(filePath).with_suffix('')
# Convert the 'Path' back to 'string' and 'split' into a 'list'
pathStr = str(filePath).split("/")
# The file name is last element of the 'list'
tabTitle = pathStr[-1]
# SCOND: trim length to 12 characters.
# Nte: we use the tab 'tooltip' to show 'path' and 'file name'
length = len(tabTitle)
if length > 12:
tabTitle1 = tabTitle[0:3]
tabTitle2 = tabTitle[length - 6:length]
tabTitle = tabTitle1 + '..' + tabTitle2
return tabTitle
def trimHome(filePath):
""" Replace ${HOME} with '~' in 'filePath' """
# Note: This also works on Windows :)
home = expanduser("~")
trimmedPath = filePath.replace(home, "~")
return trimmedPath
Any advice or help to on how to implement an Edit Menu while maintaining the Tab functionality would be welcome.