# -*- coding: utf-8 -*-
### Copyright (C) 2008-2012 Antonio Valentino <a_valentino@users.sf.net>
### This file is part of GSDView.
### GSDView 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.
### GSDView is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
### GNU General Public License for more details.
### You should have received a copy of the GNU General Public License
### along with GSDView; if not, write to the Free Software
### Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
'''GUI front-end for the Geospatial Data Abstracton Library (GDAL).'''
import os
import sys
import logging
from qt import QtCore, QtGui
from exectools.qt4 import Qt4ToolController, Qt4DialogLoggingHandler
from . import info
from . import utils
from . import errors
from . import qt4support
from . import graphicsview
from . import mousemanager
from . import pluginmanager
from .mdi import ItemModelMainWindow
from .appsite import USERCONFIGDIR, SYSPLUGINSDIR
from .widgets import AboutDialog, PreferencesDialog
from .widgets import GSDViewExceptionDialog as ExceptionDialog
__author__ = 'Antonio Valentino <a_valentino@users.sf.net>'
__date__ = '$Date$'
__revision__ = '$Revision$'
__all__ = ['GSDView']
[docs]class GSDView(ItemModelMainWindow):
# @TODO:
# * cache browser, cache cleanup
# * open internal product
# * disable actions when the external tool is running
# * /usr/share/doc/python-qt4-doc/examples/mainwindows/recentfiles.py
'''Main window class for GSDView application.'''
def __init__(self, parent=None, flags=QtCore.Qt.WindowFlags(0), **kwargs):
logger = logging.getLogger('gsdview')
logger.debug('Main window base classes initialization ...')
QtGui.QApplication.setWindowIcon(
qt4support.geticon('GSDView.png', __name__))
super(GSDView, self).__init__(parent, flags, **kwargs)
title = self.tr('GSDView Open Source Edition v. %s') % info.version
self.setWindowTitle(title)
self.setObjectName('gsdview-mainwin')
# Dialogs
logger.debug('Setting up file dialog ...')
#: application global file dialog instance
self.filedialog = QtGui.QFileDialog(self)
self.filedialog.setFileMode(QtGui.QFileDialog.ExistingFile)
self.filedialog.setViewMode(QtGui.QFileDialog.Detail)
logger.debug('Setting up the about dialog ...')
#: application global about dialog instance
self.aboutdialog = AboutDialog(self)
logger.debug('Setting up the preferences dialog ...')
#: prefernces dialog instance
self.preferencesdialog = PreferencesDialog(self,
apply=self.applySettings)
# Stop button
logger.debug('Setting up the stop button ...')
qstyle = QtGui.QApplication.style()
icon = qstyle.standardIcon(QtGui.QStyle.SP_BrowserStop)
#: stop button for external tools
self.stopbutton = QtGui.QPushButton(icon, self.tr('Stop'), self)
self.statusBar().addPermanentWidget(self.stopbutton)
self.stopbutton.hide()
# Progressbar
logger.debug('Setting up the progress bar ...')
#: application progress bar
self.progressbar = QtGui.QProgressBar(self)
self.progressbar.setTextVisible(True)
self.statusBar().addPermanentWidget(self.progressbar)
self.progressbar.hide()
# Miscellanea
logger.debug('Miscellanea setup ...')
#: cache directory path
self.cachedir = None
# GraphicsViewMonitor and mouse manager
logger.debug('Setting up "monitor" components ...')
#: graphics scenes/views monitor
self.monitor = graphicsview.GraphicsViewMonitor()
#: mouse manager for graphics scenes/views
self.mousemanager = mousemanager.MouseManager(self)
self.mousemanager.mode = 'hand'
# Plugin Manager
#: backends list
self.backends = []
#: plugin manager instance
self.pluginmanager = pluginmanager.PluginManager(self, SYSPLUGINSDIR)
self.preferencesdialog.addPage(
pluginmanager.PluginManagerGui(self.pluginmanager, self),
qt4support.geticon('plugin.svg', __name__),
label='Plugins')
# Settings
if not os.path.isdir(USERCONFIGDIR):
os.makedirs(USERCONFIGDIR)
# @TODO: fix filename
logger.debug('Read application settings ...')
#self.settings = QtCore.QSettings('gsdview-soft', 'gsdview', self)
#self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat,
# QtCore.QSettings.UserScope,
# 'gsdview', 'gsdview', self)
cfgfile = os.path.join(USERCONFIGDIR, 'gsdview.ini')
logger.info('Configuration file: "%s".', cfgfile)
#: application settings
self.settings = QtCore.QSettings(cfgfile,
QtCore.QSettings.IniFormat,
self)
# Setup the log system and the external tools controller
logger.debug('Complete logging setup...')
# @TODO: logevel could be set from command line
#: application sandard logger
self.logger = self.setupLogging()
logger.debug('Setting up external tool controller ...')
#: external tool controller
self.controller = self.setupController(self.logger, self.statusBar(),
self.progressbar)
# Actions
logger.debug('Setting up actions ...')
#: actions associated to file menu
self.fileActions = None
#: settings actions
self.settingsActions = None
#: help actions
self.helpActions = None
self.setupActions()
# File menu end toolbar
self._addMenuFromActions(self.fileActions, self.tr('&File'))
self._addToolBarFromActions(self.fileActions, self.tr('File toolbar'))
# Image menu and toolbar
self.imagemenu = self._addMenuFromActions(self.mousemanager.actions,
self.tr('&Image'))
self._addToolBarFromActions(self.mousemanager.actions,
self.tr('Mouse toolbar'))
# Tools menu
self.toolsmenu = QtGui.QMenu(self.tr('&Tools'), self)
self.menuBar().addMenu(self.toolsmenu)
self.toolsmenu.hide()
# Setup plugins
logger.debug(self.tr('Setup plugins ...'))
self.setupPlugins()
# Settings menu end toolbar
logger.debug(self.tr('Settings menu setup ...'))
menu = self._addMenuFromActions(self.settingsActions,
self.tr('&Settings'))
self._addToolBarFromActions(self.settingsActions,
self.tr('Settings toolbar'))
#: settings sub-menu
self.settings_submenu = QtGui.QMenu(self.tr('&View'),
aboutToShow=self.updateSettingsMenu)
menu.addSeparator()
menu.addMenu(self.settings_submenu)
logger.debug(self.tr('Window menu setup ...'))
self.menuBar().addMenu(self.windowmenu)
# Help menu end toolbar
logger.debug('Help menu setup ...')
self._addMenuFromActions(self.helpActions, self.tr('&Help'))
self._addToolBarFromActions(self.helpActions, self.tr('Help toolbar'))
# @NOTE: the window state setup must happen after the plugins loading
logger.info('Load settings ...')
self.loadSettings() # @TODO: pass settings
# @TODO: force the log level set from command line
#self.logger.setLevel(level)
self.treeview.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.treeview.customContextMenuRequested.connect(self.itemContextMenu)
self.statusBar().showMessage('Ready')
### Model/View utils ######################################################
[docs] def currentGraphicsView(self):
window = self.mdiarea.activeSubWindow()
if window:
widget = window.widget()
if isinstance(widget, QtGui.QGraphicsView):
return widget
return None
@QtCore.Slot(QtCore.QPoint)
[docs] def closeEvent(self, event):
self.controller.stop_tool()
# @TODO: whait for finished (??)
# @TODO: save opened datasets (??)
self.saveSettings()
self.pluginmanager.save_settings(self.settings)
self.closeAll()
self.pluginmanager.reset()
self.logger.info('Closing application')
#event.accept()
super(GSDView, self).closeEvent(event)
[docs] def changeEvent(self, event):
try:
if event.oldState() == QtCore.Qt.WindowNoState:
# save window size and position
self.settings.beginGroup('mainwindow')
self.settings.setValue('position', self.pos())
self.settings.setValue('size', self.size())
self.settings.endGroup()
event.accept()
except AttributeError:
pass
### Custom exception hook #################################################
[docs] def excepthook(self, exctype, excvalue, tracebackobj):
'''Global function to catch unhandled exceptions.
:param exctype:
exception class
:param excvalue:
exception instance
:param tracebackobj:
traceback object
'''
sys.__excepthook__(exctype, excvalue, tracebackobj)
# No messages for keyboard interruts
if not issubclass(exctype, Exception):
#~ if issubclass(exctype, KeyboardInterrupt):
msg = str(excvalue)
if not msg:
msg = excvalue.__class__.__name__
self.logger.info(msg)
self.close()
return
# #TODO: check
# Guard for avoiding multiple dialog opening
if hasattr(self, '_busy'):
return
self._busy = True
# @TODO: sometimes a RuntimeError is raised claiming that the
# "underlying C/C++ object has been deleted".
# Try to build the dialog without parent (self) and check
# modality.
dialog = ExceptionDialog(exctype, excvalue, tracebackobj, self)
#dialog.show()
ret = dialog.exec_()
if ret == QtGui.QDialog.Rejected:
self.close()
else:
logging.warning('ignoring an unhandled exception may cause '
'program malfunctions.')
### Setup helpers #########################################################
def _setupFileActions(self):
# @TODO: add a "close all" (items) action
actionsgroup = QtGui.QActionGroup(self)
# Open
icon = qt4support.geticon('open.svg', __name__)
QtGui.QAction(icon, self.tr('&Open'), actionsgroup,
objectName='openAction',
shortcut=self.tr('Ctrl+O'),
toolTip=self.tr('Open an existing file'),
statusTip=self.tr('Open an existing file'),
triggered=self.openFile)
# Close
icon = qt4support.geticon('close.svg', __name__)
QtGui.QAction(icon, self.tr('&Close'), actionsgroup,
objectName='closeAction',
# 'Ctrl+W' shortcu is used for closing windows
#shortcut=self.tr('Ctrl+W'),
toolTip=self.tr('Close the current file'),
statusTip=self.tr('Close the current file'),
triggered=self.closeItem)
# Separator
QtGui.QAction(actionsgroup).setSeparator(True)
# objectName='separator')
# Exit
icon = qt4support.geticon('quit.svg', __name__)
QtGui.QAction(icon, self.tr('&Exit'), actionsgroup,
objectName='exitAction',
shortcut=self.tr('Ctrl+X'),
toolTip=self.tr('Exit the program'),
statusTip=self.tr('Exit the program'),
triggered=self.close)
return actionsgroup
def _setupSettingsActions(self):
actionsgroup = QtGui.QActionGroup(self)
# Preferences
icon = qt4support.geticon('preferences.svg', __name__)
QtGui.QAction(icon, self.tr('&Preferences'), actionsgroup,
objectName='preferencesAction',
toolTip=self.tr('Open the program preferences dialog'),
statusTip=self.tr('Open the program preferences dialog'),
triggered=self.showPreferencesDialog)
icon = qt4support.geticon('full-screen.svg', __name__)
QtGui.QAction(icon, self.tr('&Full Screen'), actionsgroup,
objectName='fullScreenAction',
shortcut='Ctrl+Meta+F',
toolTip=self.tr('Toggle full screen mode'),
statusTip=self.tr('Toggle full screen mode'),
triggered=self.toggleFullScreenMode)
return actionsgroup
def _setupHelpActions(self):
actionsgroup = QtGui.QActionGroup(self)
# About
icon = qt4support.geticon('about.svg', __name__)
QtGui.QAction(icon, self.tr('&About'), actionsgroup,
objectName='aboutAction',
toolTip=self.tr('Show program information'),
statusTip=self.tr('Show program information'),
triggered=lambda: self.aboutdialog.exec_())
# AboutQt
icon = QtGui.QIcon(':/trolltech/qmessagebox/images/qtlogo-64.png')
QtGui.QAction(icon, self.tr('About &Qt'), actionsgroup,
objectName='aboutQtAction',
toolTip=self.tr('Show information about Qt'),
statusTip=self.tr('Show information about Qt'),
triggered=lambda: QtGui.QMessageBox.aboutQt(self))
return actionsgroup
[docs] def setupActions(self):
self.fileActions = self._setupFileActions()
self.settingsActions = self._setupSettingsActions()
self.helpActions = self._setupHelpActions()
# @TODO: tree view actions: expand/collapse all, expand/collapse
# subtree
# @TODO: stop action
def _addMenuFromActions(self, actions, name):
menu = qt4support.actionGroupToMenu(actions, name, self)
self.menuBar().addMenu(menu)
return menu
def _addToolBarFromActions(self, actions, name):
toolbar = qt4support.actionGroupToToolbar(actions, name)
self.addToolBar(toolbar)
# @COMPATIBILITY: pyside 1.0.1
# without the call to toolbar.parent() the toolbar is
# not actually added
assert toolbar.parent()
return toolbar
[docs] def setupPlugins(self):
# load backends
# @WARNING: (pychecker) Function (__import__) doesn't support **kwArgs
module = __import__('gsdview.gdalbackend', fromlist=['gsdview'])
self.pluginmanager.load_module(module, 'gdalbackend')
# load settings
self.pluginmanager.load_settings(self.settings)
# save initial state
self.pluginmanager.save_settings(self.settings)
[docs] def setupLogging(self):
logger = logging.getLogger('gsdview') # 'gsdview' # @TODO: fix
# move this to launch.py
fmt = ('%(levelname)s: %(asctime)s %(filename)s line %(lineno)d in '
'%(funcName)s: %(message)s')
formatter = logging.Formatter(fmt)
logfile = os.path.join(USERCONFIGDIR, 'gsdview.log')
handler = logging.FileHandler(logfile, 'w')
handler.setLevel(logging.DEBUG)
handler.setFormatter(formatter)
logger.addHandler(handler)
formatter = logging.Formatter('%(message)s')
handler = Qt4DialogLoggingHandler(parent=self, dialog=None)
handler.setLevel(logging.WARNING)
handler.setFormatter(formatter)
logger.addHandler(handler)
# set log level
# @WARNING: duplicate loadSettings
level = self.settings.value('preferences/loglevel', 'INFO')
levelno = logging.getLevelName(str(level))
if isinstance(levelno, int):
logger.setLevel(levelno)
logger.info('"%s" loglevel set' % level)
else:
logging.debug('invalid log level: "%s"' % level)
return logger
[docs] def setupController(self, logger, statusbar, progressbar):
controller = Qt4ToolController(logger, parent=self)
controller.subprocess.started.connect(self.processingStarted)
controller.finished.connect(self.processingDone)
self.stopbutton.clicked.connect(controller.stop_tool)
return controller
### Settings ##############################################################
def _restoreWindowState(self, settings=None):
if settings is None:
settings = self.settings
settings.beginGroup('mainwindow')
try:
position = settings.value('position')
if position is not None:
self.move(position)
size = settings.value('size')
if size is not None:
self.resize(size)
else:
# default size
self.resize(800, 600)
try:
winstate = settings.value('winstate', QtCore.Qt.WindowNoState)
if winstate and winstate != QtCore.Qt.WindowNoState:
# @COMPATIBILITY: presumably a bug in PyQt4 4.7.2
# @TODO: check
winstate = qt4support.intToWinState[winstate]
self.setWindowState(winstate)
except KeyError:
logging.debug('unable to restore the window state',
exc_info=True)
# State of toolbars ad docks
state = settings.value('state')
if state is not None:
self.restoreState(state)
finally:
settings.endGroup()
def _restoreFileDialogState(self, settings=None):
if settings is None:
settings = self.settings
settings.beginGroup('filedialog')
try:
# state
state = settings.value('state')
if state is not None:
try:
# QFileDialog.restoreState is new in Qt 4.3
self.filedialog.restoreState(state)
except AttributeError:
logging.debug('unable to save the file dialog state')
# workdir
workdir = settings.value('workdir', utils.default_workdir())
workdir = os.path.expanduser(os.path.expandvars(workdir))
self.filedialog.setDirectory(workdir)
# history
#history = settings.value('history')
#if history:
# self.filedialog.setHistory(history)
# sidebar urls
try:
# QFileDialog.setSidebarUrls is new in Qt 4.3
sidebarurls = settings.value('sidebarurls')
if sidebarurls:
sidebarurls = [QtCore.QUrl(item) for item in sidebarurls]
self.filedialog.setSidebarUrls(sidebarurls)
except AttributeError:
logging.debug('unable to restore sidebar URLs of the file '
'dialog')
finally:
settings.endGroup()
[docs] def loadSettings(self, settings=None):
# @TODO: split app saveSettings frlm plugins one
if settings is None:
settings = self.settings
# general
self._restoreWindowState(settings)
self._restoreFileDialogState(settings)
settings.beginGroup('preferences')
try:
# log level
level = settings.value('loglevel', 'INFO')
levelno = logging.getLevelName(level)
if isinstance(levelno, int):
self.logger.setLevel(levelno)
self.logger.debug('"%s" loglevel set' % level)
else:
logging.debug('invalid log level: "%s"' % level)
default = os.path.join(USERCONFIGDIR, 'cache')
cachedir = settings.value('cachedir', default)
self.cachedir = os.path.expanduser(os.path.expandvars(cachedir))
finally:
settings.endGroup()
# cache
# @TODO
# plugins
for plugin in self.pluginmanager.plugins.itervalues():
plugin.loadSettings(settings)
def _saveWindowState(self, settings=None):
if settings is None:
settings = self.settings
settings.beginGroup('mainwindow')
try:
settings.setValue('winstate', self.windowState())
if self.windowState() == QtCore.Qt.WindowNoState:
settings.setValue('position', self.pos())
settings.setValue('size', self.size())
settings.setValue('state', self.saveState())
finally:
settings.endGroup()
def _saveFileDialogState(self, settings=None):
if settings is None:
settings = self.settings
settings.beginGroup('filedialog')
try:
# state
try:
# QFileDialog.saveState is new in Qt 4.3
settings.setValue('state', self.filedialog.saveState())
except AttributeError:
logging.debug('unable to save the file dialog state')
# workdir
# @NOTE: uncomment to preserve the session value
#workdir = settings.setValue('workdir',
# self.filedialog.directory())
# history
#settings.setValue('history', self.filedialog.history())
# sidebar urls
try:
# QFileDialog.sidebarUrls is new in Qt 4.3
sidebarurls = self.filedialog.sidebarUrls()
if sidebarurls:
settings.setValue('sidebarurls', sidebarurls)
except AttributeError:
logging.debug('unable to save sidebar URLs of the file dialog')
finally:
settings.endGroup()
[docs] def saveSettings(self, settings=None):
if settings is None:
settings = self.settings
# general
self._saveWindowState(settings)
self._saveFileDialogState(settings)
settings.beginGroup('preferences')
try:
level = logging.getLevelName(self.logger.level)
settings.setValue('loglevel', level)
# only changed via preferences
#settings.setValue('cachedir', self.cachedir)
finally:
settings.endGroup()
# @NOTE: cache preferences are only modified via preferences dialog
for plugin in self.pluginmanager.plugins.itervalues():
#logging.debug('save %s plugin preferences' % plugin.name)
plugin.saveSettings(settings)
@QtCore.Slot()
[docs] def toggleFullScreenMode(self):
self.setWindowState(self.windowState() ^ QtCore.Qt.WindowFullScreen)
@QtCore.Slot()
@QtCore.Slot()
[docs] def applySettings(self):
self.preferencesdialog.save(self.settings)
self.loadSettings()
@QtCore.Slot()
[docs] def showPreferencesDialog(self):
# @TODO: complete
self.saveSettings()
self.preferencesdialog.load(self.settings)
if self.preferencesdialog.exec_():
self.applySettings()
### File actions ##########################################################
@QtCore.Slot()
[docs] def openFile(self):
# @TODO: remove; this is a temporary workaround for a Qt bug in
# Cocoa version
self.filedialog.selectNameFilter(self.filedialog.selectedNameFilter())
# @TODO: allow multiple file selection
if self.filedialog.exec_():
filename = str(self.filedialog.selectedFiles()[0])
if filename:
for backendname in self.backends:
backend = self.pluginmanager.plugins[backendname]
try:
item = backend.openFile(filename)
if item:
self.datamodel.appendRow(item)
self.treeview.expand(item.index())
self.logger.debug('File "%s" opened with backend '
'"%s"' % (filename, backendname))
else:
self.logger.info('file %s" already open' %
filename)
break
except errors.OpenError:
#self.logger.exception('exception caught')
self.logger.debug('Backend "%s" failed to open file '
'"%s"' % (backendname, filename))
else:
self.logger.error('Unable to open file "%s"' % filename)
@QtCore.Slot()
[docs] def closeItem(self):
# @TODO: extend for multiple datasets
#~ self.closeGdalDataset.emit()
item = self.currentItem()
if item:
# find the toplevel item
while item.parent():
item = item.parent()
try:
#~ backend = self.pluginmanager.plugins[item.backend]
#~ backend.closeFile(item)
item.close()
except AttributeError:
self.datamodel.invisibleRootItem().removeRow(item.row())
self.statusBar().showMessage('Ready.')
@QtCore.Slot()
[docs] def closeAll(self):
root = self.datamodel.invisibleRootItem()
while root.hasChildren():
item = root.child(0)
try:
#~ backend = self.pluginmanager.plugins[item.backend]
#~ backend.closeFile(item)
item.close()
except AttributeError:
root.removeRow(item.row())
### Auxiliary methods ####################################################
@QtCore.Slot()
@QtCore.Slot(str)
[docs] def processingStarted(self, msg=None):
if msg:
self.statusBar().showMessage(msg)
self.progressbar.show()
self.stopbutton.setEnabled(True)
self.stopbutton.show()
@QtCore.Slot(int)
[docs] def updateProgressBar(self, fract):
self.progressbar.show()
self.progressbar.setValue(int(100. * fract))
@QtCore.Slot(int)
[docs] def processingDone(self, returncode=0):
#self.controller.reset() # @TODO: remove
try:
if returncode != 0:
msg = ('An error occurred during the quicklook generation.\n'
'Now close the dataset.')
QtGui.QMessageBox.warning(self, '', msg)
self.closeItem() # @TODO: check
finally:
self.progressbar.hide()
self.stopbutton.setEnabled(False)
self.stopbutton.hide()
self.statusBar().showMessage('Ready.')