8

I have a problem related to this question and the script given as the answer. I can add some functionalities to Attribute Table (AT) by the script in the post. But according to the post, I have to open the AT using showAttributeTable method, because the method returns the reference of the AT created/opened.

Of course, I can get references of all opened ATs using the following line.

tables = [w for w in qApp.allWidgets() if w.objectName() == 'AttributeTable']

Let's say, I want to add a button to all ATs to be opened or sort by a column for a specific layer. Somehow, I think I need to catch "the AT's opening event" or "a widget/child was added to iface.mainWindow()" etc.

I have looked at Qt5 and QGIS API(QgsApplication) documentation, but I couldn't find anything helpful or I missed something.

A pseudo code for a possible solution:

def something_opened(something):
    if something is an attribute_table and active_layer is foo_bar:
        do something

main_window.addedsomething.connect(something_opened)

Note: @Ben's answer is pretty nice. But, it still requires me to focus on the AT.

  • Apology: This question can be partially (or maybe fully) related to Qt. But it is also related to QGIS, Therefore, I've asked in GIS.SE. – Kadir Şahbaz Apr 19 at 14:12
11
+50

Interesting question! I couldn't find any native signal emitted when an attribute table is opened or closed so I would call this solution a fairly inelegant workaround but it seems to work well enough. I found that QApplication has a focusChanged(old, new) signal which is emitted whenever the widget focus changes e.g. opening/ closing dialogs or clicking between non-modal windows etc. and returns the old and new widget objects.

class addAttributeTableAction(object):

    def __init__(self, app):
        self.app = app
        self.app.focusChanged.connect(self.attribute_dialog_opened)

    def __del__(self):
        self.app.focusChanged.disconnect(self.attribute_dialog_opened)

    def attribute_dialog_opened(self, old, new):
        if isinstance(new, QTableView):
            #I don't like the line below but I could't think of a better/quicker way to
            #return the Dialog object from the QTableView object returned by the 'new' parameter
            #of the focusChanged signal
            table_dialog = new.parent().parent().parent().parent()
            toolbar = [c for c in table_dialog.children() if isinstance(c, QToolBar)][0]
            # check if action has already been added to toolbar
            already_exists = [a for a in toolbar.actions() if a.objectName() == 'TestAction']
            if not already_exists:
                new_button = QAction('Test', table_dialog)
                new_button.setObjectName('TestAction')
                toolbar.addAction(new_button)
                new_button.triggered.connect(self.run_action)

    def run_action(self):
        '''Simple method to test action'''
        layer = iface.activeLayer()
        layer.selectByIds([1])

Test = addAttributeTableAction(qApp)
#Uncomment below andcomment above to stop listening for focusChanged signal
#del Test

Quick demo:

enter image description here

| improve this answer | |
  • Amazing! Thank you – Taras Apr 20 at 7:10
  • Thanks @Taras, no problem! – Ben W Apr 20 at 13:06
  • This answer seems the best solution I found so far. I used evetFilter but I couldn't get any result. Probably I miss something. – Kadir Şahbaz Apr 26 at 13:52
5

Here is my solution (QGIS 3.12.2, works perfectly fine under Ubuntu, I saw few QGIS crashes under Windows 10) that gives you access directly to all opened attribute tables dialogs or attribute tables of a specific layer.

Explanations :

  • Copy and run this code into the QGIS Python Console Editor
  • The code run in another thread, so, you can continue QGIS activities.
  • Modify the refresh rate if you need. Here it is 2 seconds.
  • If you want to access to attribute tables :
    • attribute_tables() returns a list with all opened attribute tables ;
    • attribute_tables(layer_id) returns the list of all opened attribute tables of the layer.
  • If you want to stop the worker (= background thread), just worker = None
#!/usr/bin/env python3

from time import sleep
from PyQt5.QtCore import QThread, pyqtSignal
from qgis.core import QgsApplication, QgsProject


class Worker(QThread):
    result = pyqtSignal(dict)

    def __init__(self, qgsapp, qgsproject):
        super(Worker, self).__init__()
        self.app = qgsapp
        self.qpj = qgsproject

    def __del__(self):
        self.wait()

    def run(self):
        while True:
            ats = {
                id(o): [o.objectName()[24:], o]
                for o in self.app.allWidgets()
                if isinstance(o, QDialog)
                and o.objectName()[:24] == "QgsAttributeTableDialog/"
            }
            lyr_at = {}
            for lyrid, dialog in ats.values():
                if lyrid in lyr_at:
                    lyr_at[lyrid].append(dialog)
                else:
                    lyr_at[lyrid] = [dialog]

            self.result.emit(lyr_at)
            sleep(2)  # refresh rate : 2 seconds


class Atat(object):
    def __init__(self):
        self._dialogs = {}

    @property
    def dialogs(self) -> dict:
        return self._dialogs

    @dialogs.setter
    def dialogs(self, value: dict):
        if isinstance(value, dict):
            self._dialogs = value


def attribute_tables(layer_id: str = None) -> list:
    global ATINST
    if not layer_id:
        return [d for ld in ATINST.dialogs.values() for d in ld]
    elif layer_id in ATINST.dialogs:
        return ATINST.dialogs[layer_id]

    return []


app = QgsApplication.instance()
qpj = QgsProject.instance()
ATINST = Atat()
worker = Worker(app, qpj)
worker.result.connect(
    lambda new_at: setattr(ATINST, 'dialogs', new_at)
)
worker.start()
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.