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