1# -*- coding: utf-8 -*-
2
3"""
4/***************************************************************************
5Name                 : DB Manager
6Description          : Database manager plugin for QGIS
7Date                 : May 23, 2011
8copyright            : (C) 2011 by Giuseppe Sucameli
9email                : brush.tyler@gmail.com
10
11The content of this file is based on
12- PG_Manager by Martin Dobias (GPLv2 license)
13 ***************************************************************************/
14
15/***************************************************************************
16 *                                                                         *
17 *   This program is free software; you can redistribute it and/or modify  *
18 *   it under the terms of the GNU General Public License as published by  *
19 *   the Free Software Foundation; either version 2 of the License, or     *
20 *   (at your option) any later version.                                   *
21 *                                                                         *
22 ***************************************************************************/
23"""
24
25import functools
26
27from qgis.PyQt.QtCore import Qt, QByteArray, QSize
28from qgis.PyQt.QtWidgets import QMainWindow, QApplication, QMenu, QTabWidget, QGridLayout, QSpacerItem, QSizePolicy, QDockWidget, QStatusBar, QMenuBar, QToolBar, QTabBar
29from qgis.PyQt.QtGui import QIcon, QKeySequence
30
31from qgis.gui import QgsMessageBar
32from qgis.core import (
33    Qgis,
34    QgsApplication,
35    QgsSettings,
36    QgsMapLayerType
37)
38from qgis.utils import OverrideCursor
39
40from .info_viewer import InfoViewer
41from .table_viewer import TableViewer
42from .layer_preview import LayerPreview
43
44from .db_tree import DBTree
45
46from .db_plugins.plugin import BaseError
47from .dlg_db_error import DlgDbError
48
49
50class DBManager(QMainWindow):
51
52    def __init__(self, iface, parent=None):
53        QMainWindow.__init__(self, parent)
54        self.setAttribute(Qt.WA_DeleteOnClose)
55        self.setupUi()
56        self.iface = iface
57
58        # restore the window state
59        settings = QgsSettings()
60        self.restoreGeometry(settings.value("/DB_Manager/mainWindow/geometry", QByteArray(), type=QByteArray))
61        self.restoreState(settings.value("/DB_Manager/mainWindow/windowState", QByteArray(), type=QByteArray))
62
63        self.toolBar.setIconSize(self.iface.iconSize())
64        self.toolBarOrientation()
65        self.toolBar.orientationChanged.connect(self.toolBarOrientation)
66        self.tabs.currentChanged.connect(self.tabChanged)
67        self.tree.selectedItemChanged.connect(self.itemChanged)
68        self.tree.model().dataChanged.connect(self.iface.reloadConnections)
69        self.itemChanged(None)
70
71    def closeEvent(self, e):
72        self.unregisterAllActions()
73        # clear preview, this will delete the layer in preview tab
74        self.preview.loadPreview(None)
75
76        # save the window state
77        settings = QgsSettings()
78        settings.setValue("/DB_Manager/mainWindow/windowState", self.saveState())
79        settings.setValue("/DB_Manager/mainWindow/geometry", self.saveGeometry())
80
81        QMainWindow.closeEvent(self, e)
82
83    def refreshItem(self, item=None):
84        with OverrideCursor(Qt.WaitCursor):
85            try:
86                if item is None:
87                    item = self.tree.currentItem()
88                self.tree.refreshItem(item)  # refresh item children in the db tree
89            except BaseError as e:
90                DlgDbError.showError(e, self)
91
92    def itemChanged(self, item):
93        with OverrideCursor(Qt.WaitCursor):
94            try:
95                self.reloadButtons()
96                # Force-reload information on the layer
97                self.info.setDirty()
98                # clear preview, this will delete the layer in preview tab
99                self.preview.loadPreview(None)
100                self.refreshTabs()
101            except BaseError as e:
102                DlgDbError.showError(e, self)
103
104    def reloadButtons(self):
105        db = self.tree.currentDatabase()
106        if not hasattr(self, '_lastDb'):
107            self._lastDb = db
108
109        elif db == self._lastDb:
110            return
111
112        # remove old actions
113        if self._lastDb is not None:
114            self.unregisterAllActions()
115
116        # add actions of the selected database
117        self._lastDb = db
118        if self._lastDb is not None:
119            self._lastDb.registerAllActions(self)
120
121    def tabChanged(self, index):
122        with OverrideCursor(Qt.WaitCursor):
123            try:
124                self.refreshTabs()
125            except BaseError as e:
126                DlgDbError.showError(e, self)
127
128    def refreshTabs(self):
129        index = self.tabs.currentIndex()
130        item = self.tree.currentItem()
131        table = self.tree.currentTable()
132
133        # enable/disable tabs
134        self.tabs.setTabEnabled(self.tabs.indexOf(self.table), table is not None)
135        self.tabs.setTabEnabled(self.tabs.indexOf(self.preview), table is not None and table.type in [table.VectorType,
136                                                                                                      table.RasterType] and table.geomColumn is not None)
137        # show the info tab if the current tab is disabled
138        if not self.tabs.isTabEnabled(index):
139            self.tabs.setCurrentWidget(self.info)
140
141        current_tab = self.tabs.currentWidget()
142        if current_tab == self.info:
143            self.info.showInfo(item)
144        elif current_tab == self.table:
145            self.table.loadData(item)
146        elif current_tab == self.preview:
147            self.preview.loadPreview(item)
148
149    def refreshActionSlot(self):
150        self.info.setDirty()
151        self.table.setDirty()
152        self.preview.setDirty()
153        self.refreshItem()
154
155    def importActionSlot(self):
156        db = self.tree.currentDatabase()
157        if db is None:
158            self.infoBar.pushMessage(self.tr("No database selected or you are not connected to it."),
159                                     Qgis.Info, self.iface.messageTimeout())
160            return
161
162        outUri = db.uri()
163        schema = self.tree.currentSchema()
164        if schema:
165            outUri.setDataSource(schema.name, "", "", "")
166
167        from .dlg_import_vector import DlgImportVector
168
169        dlg = DlgImportVector(None, db, outUri, self)
170        dlg.exec_()
171
172    def exportActionSlot(self):
173        table = self.tree.currentTable()
174        if table is None:
175            self.infoBar.pushMessage(self.tr("Select the table you want export to file."), Qgis.Info,
176                                     self.iface.messageTimeout())
177            return
178
179        inLayer = table.toMapLayer()
180        if inLayer.type() != QgsMapLayerType.VectorLayer:
181            self.infoBar.pushMessage(
182                self.tr("Select a vector or a tabular layer you want export."),
183                Qgis.Warning, self.iface.messageTimeout())
184            return
185
186        from .dlg_export_vector import DlgExportVector
187
188        dlg = DlgExportVector(inLayer, table.database(), self)
189        dlg.exec_()
190
191        inLayer.deleteLater()
192
193    def runSqlWindow(self):
194        db = self.tree.currentDatabase()
195        if db is None:
196            self.infoBar.pushMessage(self.tr("No database selected or you are not connected to it."),
197                                     Qgis.Info, self.iface.messageTimeout())
198            # force displaying of the message, it appears on the first tab (i.e. Info)
199            self.tabs.setCurrentIndex(0)
200            return
201
202        from .dlg_sql_window import DlgSqlWindow
203
204        query = DlgSqlWindow(self.iface, db, self)
205        dbname = db.connection().connectionName()
206        tabname = self.tr("Query ({0})").format(dbname)
207        index = self.tabs.addTab(query, tabname)
208        self.tabs.setTabIcon(index, db.connection().icon())
209        self.tabs.setCurrentIndex(index)
210        query.nameChanged.connect(functools.partial(self.update_query_tab_name, index, dbname))
211
212    def runSqlLayerWindow(self, layer):
213        from .dlg_sql_layer_window import DlgSqlLayerWindow
214        query = DlgSqlLayerWindow(self.iface, layer, self)
215        lname = layer.name()
216        tabname = self.tr("Layer ({0})").format(lname)
217        index = self.tabs.addTab(query, tabname)
218        # self.tabs.setTabIcon(index, db.connection().icon())
219        self.tabs.setCurrentIndex(index)
220
221    def update_query_tab_name(self, index, dbname, queryname):
222        if not queryname:
223            queryname = self.tr("Query")
224        tabname = u"%s (%s)" % (queryname, dbname)
225        self.tabs.setTabText(index, tabname)
226
227    def showSystemTables(self):
228        self.tree.showSystemTables(self.actionShowSystemTables.isChecked())
229
230    def registerAction(self, action, menuName, callback=None):
231        """ register an action to the manager's main menu """
232        if not hasattr(self, '_registeredDbActions'):
233            self._registeredDbActions = {}
234
235        if callback is not None:
236            def invoke_callback(x):
237                return self.invokeCallback(callback)
238
239        if menuName is None or menuName == "":
240            self.addAction(action)
241
242            if menuName not in self._registeredDbActions:
243                self._registeredDbActions[menuName] = list()
244            self._registeredDbActions[menuName].append(action)
245
246            if callback is not None:
247                action.triggered.connect(invoke_callback)
248            return True
249
250        # search for the menu
251        actionMenu = None
252        helpMenuAction = None
253        for a in self.menuBar.actions():
254            if not a.menu() or a.menu().title() != menuName:
255                continue
256            if a.menu() != self.menuHelp:
257                helpMenuAction = a
258
259            actionMenu = a
260            break
261
262        # not found, add a new menu before the help menu
263        if actionMenu is None:
264            menu = QMenu(menuName, self)
265            if helpMenuAction is not None:
266                actionMenu = self.menuBar.insertMenu(helpMenuAction, menu)
267            else:
268                actionMenu = self.menuBar.addMenu(menu)
269
270        menu = actionMenu.menu()
271        menuActions = menu.actions()
272
273        # get the placeholder's position to insert before it
274        pos = 0
275        for pos in range(len(menuActions)):
276            if menuActions[pos].isSeparator() and menuActions[pos].objectName().endswith("_placeholder"):
277                menuActions[pos].setVisible(True)
278                break
279
280        if pos < len(menuActions):
281            before = menuActions[pos]
282            menu.insertAction(before, action)
283        else:
284            menu.addAction(action)
285
286        actionMenu.setVisible(True)  # show the menu
287
288        if menuName not in self._registeredDbActions:
289            self._registeredDbActions[menuName] = list()
290        self._registeredDbActions[menuName].append(action)
291
292        if callback is not None:
293            action.triggered.connect(invoke_callback)
294
295        return True
296
297    def invokeCallback(self, callback, *params):
298        """ Call a method passing the selected item in the database tree,
299                the sender (usually a QAction), the plugin mainWindow and
300                optionally additional parameters.
301
302                This method takes care to override and restore the cursor,
303                but also catches exceptions and displays the error dialog.
304        """
305        with OverrideCursor(Qt.WaitCursor):
306            try:
307                callback(self.tree.currentItem(), self.sender(), self, *params)
308            except BaseError as e:
309                # catch database errors and display the error dialog
310                DlgDbError.showError(e, self)
311
312    def unregisterAction(self, action, menuName):
313        if not hasattr(self, '_registeredDbActions'):
314            return
315
316        if menuName is None or menuName == "":
317            self.removeAction(action)
318
319            if menuName in self._registeredDbActions:
320                if self._registeredDbActions[menuName].count(action) > 0:
321                    self._registeredDbActions[menuName].remove(action)
322
323            action.deleteLater()
324            return True
325
326        for a in self.menuBar.actions():
327            if not a.menu() or a.menu().title() != menuName:
328                continue
329
330            menu = a.menu()
331            menuActions = menu.actions()
332
333            menu.removeAction(action)
334            if menu.isEmpty():  # hide the menu
335                a.setVisible(False)
336
337            if menuName in self._registeredDbActions:
338                if self._registeredDbActions[menuName].count(action) > 0:
339                    self._registeredDbActions[menuName].remove(action)
340
341                # hide the placeholder if there're no other registered actions
342                if len(self._registeredDbActions[menuName]) <= 0:
343                    for i in range(len(menuActions)):
344                        if menuActions[i].isSeparator() and menuActions[i].objectName().endswith("_placeholder"):
345                            menuActions[i].setVisible(False)
346                            break
347
348            action.deleteLater()
349            return True
350
351        return False
352
353    def unregisterAllActions(self):
354        if not hasattr(self, '_registeredDbActions'):
355            return
356
357        for menuName in self._registeredDbActions:
358            for action in list(self._registeredDbActions[menuName]):
359                self.unregisterAction(action, menuName)
360        del self._registeredDbActions
361
362    def close_tab(self, index):
363        widget = self.tabs.widget(index)
364        if widget not in [self.info, self.table, self.preview]:
365            if hasattr(widget, "close"):
366                if widget.close():
367                    self.tabs.removeTab(index)
368                    widget.deleteLater()
369            else:
370                self.tabs.removeTab(index)
371                widget.deleteLater()
372
373    def toolBarOrientation(self):
374        button_style = Qt.ToolButtonIconOnly
375        if self.toolBar.orientation() == Qt.Horizontal:
376            button_style = Qt.ToolButtonTextBesideIcon
377
378        widget = self.toolBar.widgetForAction(self.actionImport)
379        widget.setToolButtonStyle(button_style)
380        widget = self.toolBar.widgetForAction(self.actionExport)
381        widget.setToolButtonStyle(button_style)
382
383    def setupUi(self):
384        self.setWindowTitle(self.tr("DB Manager"))
385        self.setWindowIcon(QIcon(":/db_manager/icon"))
386        self.resize(QSize(700, 500).expandedTo(self.minimumSizeHint()))
387
388        # create central tab widget and add the first 3 tabs: info, table and preview
389        self.tabs = QTabWidget()
390        self.info = InfoViewer(self)
391        self.tabs.addTab(self.info, self.tr("Info"))
392        self.table = TableViewer(self)
393        self.tabs.addTab(self.table, self.tr("Table"))
394        self.preview = LayerPreview(self)
395        self.tabs.addTab(self.preview, self.tr("Preview"))
396        self.setCentralWidget(self.tabs)
397
398        # display close button for all tabs but the first 3 ones, i.e.
399        # HACK: just hide the close button where not needed (GS)
400        self.tabs.setTabsClosable(True)
401        self.tabs.tabCloseRequested.connect(self.close_tab)
402        tabbar = self.tabs.tabBar()
403        for i in range(3):
404            btn = tabbar.tabButton(i, QTabBar.RightSide) if tabbar.tabButton(i, QTabBar.RightSide) else tabbar.tabButton(i, QTabBar.LeftSide)
405            btn.resize(0, 0)
406            btn.hide()
407
408        # Creates layout for message bar
409        self.layout = QGridLayout(self.info)
410        self.layout.setContentsMargins(0, 0, 0, 0)
411        spacerItem = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
412        self.layout.addItem(spacerItem, 1, 0, 1, 1)
413        # init messageBar instance
414        self.infoBar = QgsMessageBar(self.info)
415        sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
416        self.infoBar.setSizePolicy(sizePolicy)
417        self.layout.addWidget(self.infoBar, 0, 0, 1, 1)
418
419        # create database tree
420        self.dock = QDockWidget(self.tr("Providers"), self)
421        self.dock.setObjectName("DB_Manager_DBView")
422        self.dock.setFeatures(QDockWidget.DockWidgetMovable)
423        self.tree = DBTree(self)
424        self.dock.setWidget(self.tree)
425        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock)
426
427        # create status bar
428        self.statusBar = QStatusBar(self)
429        self.setStatusBar(self.statusBar)
430
431        # create menus
432        self.menuBar = QMenuBar(self)
433        self.menuDb = QMenu(self.tr("&Database"), self)
434        self.menuBar.addMenu(self.menuDb)
435        self.menuSchema = QMenu(self.tr("&Schema"), self)
436        actionMenuSchema = self.menuBar.addMenu(self.menuSchema)
437        self.menuTable = QMenu(self.tr("&Table"), self)
438        actionMenuTable = self.menuBar.addMenu(self.menuTable)
439        self.menuHelp = None  # QMenu(self.tr("&Help"), self)
440        # actionMenuHelp = self.menuBar.addMenu(self.menuHelp)
441
442        self.setMenuBar(self.menuBar)
443
444        # create toolbar
445        self.toolBar = QToolBar(self.tr("Default"), self)
446        self.toolBar.setObjectName("DB_Manager_ToolBar")
447        self.addToolBar(self.toolBar)
448
449        # create menus' actions
450
451        # menu DATABASE
452        sep = self.menuDb.addSeparator()
453        sep.setObjectName("DB_Manager_DbMenu_placeholder")
454        sep.setVisible(False)
455
456        self.actionRefresh = self.menuDb.addAction(QgsApplication.getThemeIcon("/mActionRefresh.svg"), self.tr("&Refresh"),
457                                                   self.refreshActionSlot, QKeySequence("F5"))
458        self.actionSqlWindow = self.menuDb.addAction(QIcon(":/db_manager/actions/sql_window"), self.tr("&SQL Window"),
459                                                     self.runSqlWindow, QKeySequence("F2"))
460        self.menuDb.addSeparator()
461        self.actionClose = self.menuDb.addAction(QIcon(), self.tr("&Exit"), self.close, QKeySequence("CTRL+Q"))
462
463        # menu SCHEMA
464        sep = self.menuSchema.addSeparator()
465        sep.setObjectName("DB_Manager_SchemaMenu_placeholder")
466        sep.setVisible(False)
467
468        actionMenuSchema.setVisible(False)
469
470        # menu TABLE
471        sep = self.menuTable.addSeparator()
472        sep.setObjectName("DB_Manager_TableMenu_placeholder")
473        sep.setVisible(False)
474
475        self.actionImport = self.menuTable.addAction(QIcon(":/db_manager/actions/import"),
476                                                     QApplication.translate("DBManager", "&Import Layer/File…"),
477                                                     self.importActionSlot)
478        self.actionExport = self.menuTable.addAction(QIcon(":/db_manager/actions/export"),
479                                                     QApplication.translate("DBManager", "&Export to File…"),
480                                                     self.exportActionSlot)
481        self.menuTable.addSeparator()
482        # self.actionShowSystemTables = self.menuTable.addAction(self.tr("Show system tables/views"), self.showSystemTables)
483        # self.actionShowSystemTables.setCheckable(True)
484        # self.actionShowSystemTables.setChecked(True)
485        actionMenuTable.setVisible(False)
486
487        # add actions to the toolbar
488        self.toolBar.addAction(self.actionRefresh)
489        self.toolBar.addAction(self.actionSqlWindow)
490        self.toolBar.addSeparator()
491        self.toolBar.addAction(self.actionImport)
492        self.toolBar.addAction(self.actionExport)
493