mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-24 00:47:57 -05:00
505 lines
20 KiB
Python
505 lines
20 KiB
Python
"""
|
|
/***************************************************************************
|
|
Name : DB Manager
|
|
Description : Database manager plugin for QGIS
|
|
Date : May 23, 2011
|
|
copyright : (C) 2011 by Giuseppe Sucameli
|
|
email : brush.tyler@gmail.com
|
|
|
|
The content of this file is based on
|
|
- PG_Manager by Martin Dobias (GPLv2 license)
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
"""
|
|
|
|
import functools
|
|
|
|
from qgis.PyQt.QtCore import Qt, QByteArray, QSize
|
|
from qgis.PyQt.QtWidgets import QAction, QMainWindow, QApplication, QMenu, QTabWidget, QGridLayout, QSpacerItem, QSizePolicy, QDockWidget, QStatusBar, QMenuBar, QToolBar, QTabBar
|
|
from qgis.PyQt.QtGui import QIcon, QKeySequence
|
|
|
|
from qgis.gui import QgsMessageBar
|
|
from qgis.core import (
|
|
Qgis,
|
|
QgsApplication,
|
|
QgsSettings,
|
|
QgsMapLayerType
|
|
)
|
|
from qgis.utils import OverrideCursor
|
|
|
|
from .info_viewer import InfoViewer
|
|
from .table_viewer import TableViewer
|
|
from .layer_preview import LayerPreview
|
|
|
|
from .db_tree import DBTree
|
|
|
|
from .db_plugins.plugin import BaseError
|
|
from .dlg_db_error import DlgDbError
|
|
from .gui_utils import GuiUtils
|
|
|
|
|
|
class DBManager(QMainWindow):
|
|
|
|
def __init__(self, iface, parent=None):
|
|
QMainWindow.__init__(self, parent)
|
|
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
|
self.setupUi()
|
|
self.iface = iface
|
|
|
|
# restore the window state
|
|
settings = QgsSettings()
|
|
self.restoreGeometry(settings.value("/DB_Manager/mainWindow/geometry", QByteArray(), type=QByteArray))
|
|
self.restoreState(settings.value("/DB_Manager/mainWindow/windowState", QByteArray(), type=QByteArray))
|
|
|
|
self.toolBar.setIconSize(self.iface.iconSize())
|
|
self.toolBarOrientation()
|
|
self.toolBar.orientationChanged.connect(self.toolBarOrientation)
|
|
self.tabs.currentChanged.connect(self.tabChanged)
|
|
self.tree.selectedItemChanged.connect(self.itemChanged)
|
|
self.tree.model().dataChanged.connect(self.iface.reloadConnections)
|
|
self.itemChanged(None)
|
|
|
|
def closeEvent(self, e):
|
|
self.unregisterAllActions()
|
|
# clear preview, this will delete the layer in preview tab
|
|
self.preview.loadPreview(None)
|
|
|
|
# save the window state
|
|
settings = QgsSettings()
|
|
settings.setValue("/DB_Manager/mainWindow/windowState", self.saveState())
|
|
settings.setValue("/DB_Manager/mainWindow/geometry", self.saveGeometry())
|
|
|
|
QMainWindow.closeEvent(self, e)
|
|
|
|
def refreshItem(self, item=None):
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
try:
|
|
if item is None:
|
|
item = self.tree.currentItem()
|
|
self.tree.refreshItem(item) # refresh item children in the db tree
|
|
except BaseError as e:
|
|
DlgDbError.showError(e, self)
|
|
|
|
def itemChanged(self, item):
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
try:
|
|
self.reloadButtons()
|
|
# Force-reload information on the layer
|
|
self.info.setDirty()
|
|
# clear preview, this will delete the layer in preview tab
|
|
self.preview.loadPreview(None)
|
|
self.refreshTabs()
|
|
except BaseError as e:
|
|
DlgDbError.showError(e, self)
|
|
|
|
def reloadButtons(self):
|
|
db = self.tree.currentDatabase()
|
|
if not hasattr(self, '_lastDb'):
|
|
self._lastDb = db
|
|
|
|
elif db == self._lastDb:
|
|
return
|
|
|
|
# remove old actions
|
|
if self._lastDb is not None:
|
|
self.unregisterAllActions()
|
|
|
|
# add actions of the selected database
|
|
self._lastDb = db
|
|
if self._lastDb is not None:
|
|
self._lastDb.registerAllActions(self)
|
|
|
|
def tabChanged(self, index):
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
try:
|
|
self.refreshTabs()
|
|
except BaseError as e:
|
|
DlgDbError.showError(e, self)
|
|
|
|
def refreshTabs(self):
|
|
index = self.tabs.currentIndex()
|
|
item = self.tree.currentItem()
|
|
table = self.tree.currentTable()
|
|
|
|
# enable/disable tabs
|
|
self.tabs.setTabEnabled(self.tabs.indexOf(self.table), table is not None)
|
|
self.tabs.setTabEnabled(self.tabs.indexOf(self.preview), table is not None and table.type in [table.VectorType,
|
|
table.RasterType] and table.geomColumn is not None)
|
|
# show the info tab if the current tab is disabled
|
|
if not self.tabs.isTabEnabled(index):
|
|
self.tabs.setCurrentWidget(self.info)
|
|
|
|
current_tab = self.tabs.currentWidget()
|
|
if current_tab == self.info:
|
|
self.info.showInfo(item)
|
|
elif current_tab == self.table:
|
|
self.table.loadData(item)
|
|
elif current_tab == self.preview:
|
|
self.preview.loadPreview(item)
|
|
|
|
def refreshActionSlot(self):
|
|
self.info.setDirty()
|
|
self.table.setDirty()
|
|
self.preview.setDirty()
|
|
self.refreshItem()
|
|
|
|
def importActionSlot(self):
|
|
db = self.tree.currentDatabase()
|
|
if db is None:
|
|
self.infoBar.pushMessage(self.tr("No database selected or you are not connected to it."),
|
|
Qgis.MessageLevel.Info, self.iface.messageTimeout())
|
|
return
|
|
|
|
outUri = db.uri()
|
|
schema = self.tree.currentSchema()
|
|
if schema:
|
|
outUri.setDataSource(schema.name, "", "", "")
|
|
|
|
from .dlg_import_vector import DlgImportVector
|
|
|
|
dlg = DlgImportVector(None, db, outUri, self)
|
|
dlg.exec()
|
|
|
|
def exportActionSlot(self):
|
|
table = self.tree.currentTable()
|
|
if table is None:
|
|
self.infoBar.pushMessage(self.tr("Select the table you want export to file."), Qgis.MessageLevel.Info,
|
|
self.iface.messageTimeout())
|
|
return
|
|
|
|
inLayer = table.toMapLayer()
|
|
if inLayer.type() != QgsMapLayerType.VectorLayer:
|
|
self.infoBar.pushMessage(
|
|
self.tr("Select a vector or a tabular layer you want export."),
|
|
Qgis.MessageLevel.Warning, self.iface.messageTimeout())
|
|
return
|
|
|
|
from .dlg_export_vector import DlgExportVector
|
|
|
|
dlg = DlgExportVector(inLayer, table.database(), self)
|
|
dlg.exec()
|
|
|
|
inLayer.deleteLater()
|
|
|
|
def runSqlWindow(self):
|
|
db = self.tree.currentDatabase()
|
|
if db is None:
|
|
self.infoBar.pushMessage(self.tr("No database selected or you are not connected to it."),
|
|
Qgis.MessageLevel.Info, self.iface.messageTimeout())
|
|
# force displaying of the message, it appears on the first tab (i.e. Info)
|
|
self.tabs.setCurrentIndex(0)
|
|
return
|
|
|
|
from .dlg_sql_window import DlgSqlWindow
|
|
|
|
query = DlgSqlWindow(self.iface, db, self)
|
|
dbname = db.connection().connectionName()
|
|
tabname = self.tr("Query ({0})").format(dbname)
|
|
index = self.tabs.addTab(query, tabname)
|
|
self.tabs.setTabIcon(index, db.connection().icon())
|
|
self.tabs.setCurrentIndex(index)
|
|
query.nameChanged.connect(functools.partial(self.update_query_tab_name, index, dbname))
|
|
|
|
def runSqlLayerWindow(self, layer):
|
|
from .dlg_sql_layer_window import DlgSqlLayerWindow
|
|
query = DlgSqlLayerWindow(self.iface, layer, self)
|
|
lname = layer.name()
|
|
tabname = self.tr("Layer ({0})").format(lname)
|
|
index = self.tabs.addTab(query, tabname)
|
|
# self.tabs.setTabIcon(index, db.connection().icon())
|
|
self.tabs.setCurrentIndex(index)
|
|
|
|
def update_query_tab_name(self, index, dbname, queryname):
|
|
if not queryname:
|
|
queryname = self.tr("Query")
|
|
tabname = "%s (%s)" % (queryname, dbname)
|
|
self.tabs.setTabText(index, tabname)
|
|
|
|
def showSystemTables(self):
|
|
self.tree.showSystemTables(self.actionShowSystemTables.isChecked())
|
|
|
|
def registerAction(self, action, menuName, callback=None):
|
|
""" register an action to the manager's main menu """
|
|
if not hasattr(self, '_registeredDbActions'):
|
|
self._registeredDbActions = {}
|
|
|
|
if callback is not None:
|
|
def invoke_callback(x):
|
|
return self.invokeCallback(callback)
|
|
|
|
if menuName is None or menuName == "":
|
|
self.addAction(action)
|
|
|
|
if menuName not in self._registeredDbActions:
|
|
self._registeredDbActions[menuName] = list()
|
|
self._registeredDbActions[menuName].append(action)
|
|
|
|
if callback is not None:
|
|
action.triggered.connect(invoke_callback)
|
|
return True
|
|
|
|
# search for the menu
|
|
actionMenu = None
|
|
helpMenuAction = None
|
|
for a in self.menuBar.actions():
|
|
if not a.menu() or a.menu().title() != menuName:
|
|
continue
|
|
if a.menu() != self.menuHelp:
|
|
helpMenuAction = a
|
|
|
|
actionMenu = a
|
|
break
|
|
|
|
# not found, add a new menu before the help menu
|
|
if actionMenu is None:
|
|
menu = QMenu(menuName, self)
|
|
if helpMenuAction is not None:
|
|
actionMenu = self.menuBar.insertMenu(helpMenuAction, menu)
|
|
else:
|
|
actionMenu = self.menuBar.addMenu(menu)
|
|
|
|
menu = actionMenu.menu()
|
|
menuActions = menu.actions()
|
|
|
|
# get the placeholder's position to insert before it
|
|
pos = 0
|
|
for pos in range(len(menuActions)):
|
|
if menuActions[pos].isSeparator() and menuActions[pos].objectName().endswith("_placeholder"):
|
|
menuActions[pos].setVisible(True)
|
|
break
|
|
|
|
if pos < len(menuActions):
|
|
before = menuActions[pos]
|
|
menu.insertAction(before, action)
|
|
else:
|
|
menu.addAction(action)
|
|
|
|
actionMenu.setVisible(True) # show the menu
|
|
|
|
if menuName not in self._registeredDbActions:
|
|
self._registeredDbActions[menuName] = list()
|
|
self._registeredDbActions[menuName].append(action)
|
|
|
|
if callback is not None:
|
|
action.triggered.connect(invoke_callback)
|
|
|
|
return True
|
|
|
|
def invokeCallback(self, callback, *params):
|
|
""" Call a method passing the selected item in the database tree,
|
|
the sender (usually a QAction), the plugin mainWindow and
|
|
optionally additional parameters.
|
|
|
|
This method takes care to override and restore the cursor,
|
|
but also catches exceptions and displays the error dialog.
|
|
"""
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
try:
|
|
callback(self.tree.currentItem(), self.sender(), self, *params)
|
|
except BaseError as e:
|
|
# catch database errors and display the error dialog
|
|
DlgDbError.showError(e, self)
|
|
|
|
def unregisterAction(self, action, menuName):
|
|
if not hasattr(self, '_registeredDbActions'):
|
|
return
|
|
|
|
if menuName is None or menuName == "":
|
|
self.removeAction(action)
|
|
|
|
if menuName in self._registeredDbActions:
|
|
if self._registeredDbActions[menuName].count(action) > 0:
|
|
self._registeredDbActions[menuName].remove(action)
|
|
|
|
action.deleteLater()
|
|
return True
|
|
|
|
for a in self.menuBar.actions():
|
|
if not a.menu() or a.menu().title() != menuName:
|
|
continue
|
|
|
|
menu = a.menu()
|
|
menuActions = menu.actions()
|
|
|
|
menu.removeAction(action)
|
|
if menu.isEmpty(): # hide the menu
|
|
a.setVisible(False)
|
|
|
|
if menuName in self._registeredDbActions:
|
|
if self._registeredDbActions[menuName].count(action) > 0:
|
|
self._registeredDbActions[menuName].remove(action)
|
|
|
|
# hide the placeholder if there're no other registered actions
|
|
if len(self._registeredDbActions[menuName]) <= 0:
|
|
for i in range(len(menuActions)):
|
|
if menuActions[i].isSeparator() and menuActions[i].objectName().endswith("_placeholder"):
|
|
menuActions[i].setVisible(False)
|
|
break
|
|
|
|
action.deleteLater()
|
|
return True
|
|
|
|
return False
|
|
|
|
def unregisterAllActions(self):
|
|
if not hasattr(self, '_registeredDbActions'):
|
|
return
|
|
|
|
for menuName in self._registeredDbActions:
|
|
for action in list(self._registeredDbActions[menuName]):
|
|
self.unregisterAction(action, menuName)
|
|
del self._registeredDbActions
|
|
|
|
def close_tab(self, index):
|
|
widget = self.tabs.widget(index)
|
|
if widget not in [self.info, self.table, self.preview]:
|
|
if hasattr(widget, "close"):
|
|
if widget.close():
|
|
self.tabs.removeTab(index)
|
|
widget.deleteLater()
|
|
else:
|
|
self.tabs.removeTab(index)
|
|
widget.deleteLater()
|
|
|
|
def toolBarOrientation(self):
|
|
button_style = Qt.ToolButtonStyle.ToolButtonIconOnly
|
|
if self.toolBar.orientation() == Qt.Orientation.Horizontal:
|
|
button_style = Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
|
|
|
widget = self.toolBar.widgetForAction(self.actionImport)
|
|
widget.setToolButtonStyle(button_style)
|
|
widget = self.toolBar.widgetForAction(self.actionExport)
|
|
widget.setToolButtonStyle(button_style)
|
|
|
|
def setupUi(self):
|
|
self.setWindowTitle(self.tr("DB Manager"))
|
|
self.setWindowIcon(GuiUtils.get_icon("dbmanager"))
|
|
self.resize(QSize(700, 500).expandedTo(self.minimumSizeHint()))
|
|
|
|
# create central tab widget and add the first 3 tabs: info, table and preview
|
|
self.tabs = QTabWidget()
|
|
self.info = InfoViewer(self)
|
|
self.tabs.addTab(self.info, self.tr("Info"))
|
|
self.table = TableViewer(self)
|
|
self.tabs.addTab(self.table, self.tr("Table"))
|
|
self.preview = LayerPreview(self)
|
|
self.tabs.addTab(self.preview, self.tr("Preview"))
|
|
self.setCentralWidget(self.tabs)
|
|
|
|
# display close button for all tabs but the first 3 ones, i.e.
|
|
# HACK: just hide the close button where not needed (GS)
|
|
self.tabs.setTabsClosable(True)
|
|
self.tabs.tabCloseRequested.connect(self.close_tab)
|
|
tabbar = self.tabs.tabBar()
|
|
for i in range(3):
|
|
btn = tabbar.tabButton(i, QTabBar.ButtonPosition.RightSide) if tabbar.tabButton(i, QTabBar.ButtonPosition.RightSide) else tabbar.tabButton(i, QTabBar.ButtonPosition.LeftSide)
|
|
btn.resize(0, 0)
|
|
btn.hide()
|
|
|
|
# Creates layout for message bar
|
|
self.layout = QGridLayout(self.info)
|
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
spacerItem = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
self.layout.addItem(spacerItem, 1, 0, 1, 1)
|
|
# init messageBar instance
|
|
self.infoBar = QgsMessageBar(self.info)
|
|
sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
|
self.infoBar.setSizePolicy(sizePolicy)
|
|
self.layout.addWidget(self.infoBar, 0, 0, 1, 1)
|
|
|
|
# create database tree
|
|
self.dock = QDockWidget(self.tr("Providers"), self)
|
|
self.dock.setObjectName("DB_Manager_DBView")
|
|
self.dock.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable)
|
|
self.tree = DBTree(self)
|
|
self.dock.setWidget(self.tree)
|
|
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.dock)
|
|
|
|
# create status bar
|
|
self.statusBar = QStatusBar(self)
|
|
self.setStatusBar(self.statusBar)
|
|
|
|
# create menus
|
|
self.menuBar = QMenuBar(self)
|
|
self.menuDb = QMenu(self.tr("&Database"), self)
|
|
self.menuBar.addMenu(self.menuDb)
|
|
self.menuSchema = QMenu(self.tr("&Schema"), self)
|
|
actionMenuSchema = self.menuBar.addMenu(self.menuSchema)
|
|
self.menuTable = QMenu(self.tr("&Table"), self)
|
|
actionMenuTable = self.menuBar.addMenu(self.menuTable)
|
|
self.menuHelp = None # QMenu(self.tr("&Help"), self)
|
|
# actionMenuHelp = self.menuBar.addMenu(self.menuHelp)
|
|
|
|
self.setMenuBar(self.menuBar)
|
|
|
|
# create toolbar
|
|
self.toolBar = QToolBar(self.tr("Default"), self)
|
|
self.toolBar.setObjectName("DB_Manager_ToolBar")
|
|
self.addToolBar(self.toolBar)
|
|
|
|
# create menus' actions
|
|
|
|
# menu DATABASE
|
|
sep = self.menuDb.addSeparator()
|
|
sep.setObjectName("DB_Manager_DbMenu_placeholder")
|
|
sep.setVisible(False)
|
|
|
|
self.actionRefresh = QAction(QgsApplication.getThemeIcon("/mActionRefresh.svg"), self.tr("&Refresh"),
|
|
self.menuDb)
|
|
self.actionRefresh.triggered.connect(self.refreshActionSlot)
|
|
self.actionRefresh.setShortcut(QKeySequence("F5"))
|
|
self.menuDb.addAction(self.actionRefresh)
|
|
|
|
self.actionSqlWindow = QAction(GuiUtils.get_icon('mActionSQLWindow'),
|
|
self.tr("&SQL Window"),
|
|
self.menuDb)
|
|
self.actionSqlWindow.triggered.connect(self.runSqlWindow)
|
|
self.actionSqlWindow.setShortcut(QKeySequence("F2"))
|
|
self.menuDb.addAction(self.actionSqlWindow)
|
|
|
|
self.menuDb.addSeparator()
|
|
|
|
self.actionClose = QAction(QIcon(), self.tr("&Exit"), self.menuDb)
|
|
self.actionClose.triggered.connect(self.close)
|
|
self.actionClose.setShortcut(QKeySequence("CTRL+Q"))
|
|
self.menuDb.addAction(self.actionClose)
|
|
|
|
# menu SCHEMA
|
|
sep = self.menuSchema.addSeparator()
|
|
sep.setObjectName("DB_Manager_SchemaMenu_placeholder")
|
|
sep.setVisible(False)
|
|
|
|
actionMenuSchema.setVisible(False)
|
|
|
|
# menu TABLE
|
|
sep = self.menuTable.addSeparator()
|
|
sep.setObjectName("DB_Manager_TableMenu_placeholder")
|
|
sep.setVisible(False)
|
|
|
|
self.actionImport = self.menuTable.addAction(GuiUtils.get_icon("mActionDBImport"),
|
|
QApplication.translate("DBManager", "&Import Layer/File…"),
|
|
self.importActionSlot)
|
|
self.actionExport = self.menuTable.addAction(GuiUtils.get_icon("mActionDBExport"),
|
|
QApplication.translate("DBManager", "&Export to File…"),
|
|
self.exportActionSlot)
|
|
self.menuTable.addSeparator()
|
|
# self.actionShowSystemTables = self.menuTable.addAction(self.tr("Show system tables/views"), self.showSystemTables)
|
|
# self.actionShowSystemTables.setCheckable(True)
|
|
# self.actionShowSystemTables.setChecked(True)
|
|
actionMenuTable.setVisible(False)
|
|
|
|
# add actions to the toolbar
|
|
self.toolBar.addAction(self.actionRefresh)
|
|
self.toolBar.addAction(self.actionSqlWindow)
|
|
self.toolBar.addSeparator()
|
|
self.toolBar.addAction(self.actionImport)
|
|
self.toolBar.addAction(self.actionExport)
|