QGIS/python/plugins/osm/OsmUndoRedoDW.py
2011-09-15 21:33:42 +03:00

440 lines
16 KiB
Python
Executable File

"""@package OsmUndoRedoDW
This module holds evidence of user edit actions.
Such evidence exists for each loaded OSM data.
Module provides easy way how to call undo/redo actions.
"""
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *
from qgis.gui import *
from ui_OsmUndoRedoDW import Ui_OsmUndoRedoDW
from OsmDatabaseManager import OsmDatabaseManager
import sqlite3
from math import *
from time import *
class OsmUndoRedoDW(QDockWidget, Ui_OsmUndoRedoDW, object):
"""This class extends functionality of Ui_OsmUndoRedoDW dialog which displays history of user edit actions.
Such history exists for each loaded OSM data.
This class provides easy way how to call undo/redo actions.
"""
def __init__(self, plugin):
"""The constructor.
Does basic initialization, connecting dialog signals to appropriate slots
and setting icons to its buttons.
@param plugin pointer to OSM Plugin instance
"""
QDockWidget.__init__(self, None)
self.setupUi(self)
self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.undoButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_undo.png"))
self.redoButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_redo.png"))
self.clearButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_clearUndoRedo.png"))
self.canvas=plugin.canvas
self.iface=plugin.iface
self.dbm=plugin.dbm
self.plugin=plugin
self.actionList.setDragDropMode(QAbstractItemView.NoDragDrop)
QObject.connect(self.undoButton,SIGNAL("clicked()"),self.undo)
QObject.connect(self.redoButton,SIGNAL("clicked()"),self.redo)
QObject.connect(self.clearButton,SIGNAL("clicked()"),self.clear)
QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
# structures for evidence of user edit changes (required by undo and redo operations)
self.mapActions={}
self.mapIxAction={}
self.currentMapKey=None
self.actStartId=self.actStopId=self.actNote=None
self.actionInProgress=False
self.redoCounter=0
self.undoCounter=0
self.affected=set()
self.urIsBusy=False
self.actionList.addItem(QString("<empty>"))
self.actionList.setCurrentRow(0)
def currRowChanged(self,row):
"""This function is called after currentRowChanged(...) signal is emmited on dialog's list of edit actions.
Functions calls as many undo actions (as many redo actions) as needed to jump in editing history to selected row.
@param row row number from the list of edit actions
"""
if row<0:
return
self.goToAction(row)
def setContentEnabled(self,flag):
if flag:
if self.undoCounter>0:
self.undoButton.setEnabled(True)
if self.redoCounter>0:
self.redoButton.setEnabled(True)
else:
self.undoButton.setEnabled(False)
self.redoButton.setEnabled(False)
self.clearButton.setEnabled(flag)
self.actionList.setEnabled(flag)
def clear(self):
"""Function clears (re-initializes) the whole undo/redo dialog.
"""
if self.dbm and self.dbm.currentKey:
self.dbm.removeAllChangeSteps()
self.actionList.clear()
self.actStartId=self.actStopId=self.actNote=None
self.actionInProgress=False
self.urIsBusy=False
self.actionList.addItem(QString("<empty>"))
self.actionList.setCurrentRow(0)
self.redoCounter=0
self.redoButton.setEnabled(False)
self.undoCounter=0
self.undoButton.setEnabled(False)
if self.plugin:
if self.plugin.dockWidget:
self.plugin.dockWidget.redoButton.setEnabled(False)
self.plugin.dockWidget.undoButton.setEnabled(False)
if self.currentMapKey:
self.mapActions[self.currentMapKey]=[]
self.mapIxAction[self.currentMapKey]=-1
def databaseChanged(self,dbKey):
"""Functions is called when current database of OSM Plugin changed.
OSM Undo/Redo module clears its list of actions and loads the one for new current database.
If dbKey parameter is None, there is no current database. In this case function just clears the list
and reinitialize its inner structures.
@param dbKey key/name of new current database file
"""
# clear the list widget
self.actionList.clear()
# clear inner structures
self.actStartId=self.actStopId=self.actNote=None
self.actionInProgress=False
self.currentMapKey=dbKey
self.urIsBusy=False
if not dbKey:
self.setContentEnabled(False)
return
self.setContentEnabled(True)
if dbKey in self.mapActions.keys():
# load the list widget
self.redoCounter=0
self.redoButton.setEnabled(False)
self.plugin.dockWidget.redoButton.setEnabled(False)
self.undoCounter=0
self.undoButton.setEnabled(False)
self.plugin.dockWidget.undoButton.setEnabled(False)
self.actionList.addItem(QString("<empty>"))
for action in self.mapActions[self.currentMapKey]:
self.actionList.addItem(action[2]) # 2 ~ actionNote!
ixAction=self.mapIxAction[self.currentMapKey]
self.undoCounter=ixAction+1
self.redoCounter=len(self.mapActions[self.currentMapKey])-self.undoCounter
self.actionList.setCurrentRow(ixAction+1)
if self.undoCounter>0:
self.undoButton.setEnabled(True)
self.plugin.dockWidget.undoButton.setEnabled(True)
if self.redoCounter>0:
self.redoButton.setEnabled(True)
self.plugin.dockWidget.redoButton.setEnabled(True)
return
# new dbKey has no undo/redo history yet
self.actionList.addItem(QString("<empty>"))
self.actionList.setCurrentRow(0)
self.redoCounter=0
self.redoButton.setEnabled(False)
self.plugin.dockWidget.redoButton.setEnabled(False)
self.undoCounter=0
self.undoButton.setEnabled(False)
self.plugin.dockWidget.undoButton.setEnabled(False)
self.mapActions[dbKey]=[]
self.mapIxAction[dbKey]=-1
def goToAction(self,row):
"""Functions goes to the selected row in history of edit actions.
It calls as many undo/redo operations, as needed.
@param row row index to list of edit actions history
"""
curr=self.actionList.currentRow()
self.actionList.setEnabled(False)
if not self.currentMapKey in self.mapIxAction:
return
ixGoto=row # ix of row which was clicked
ixCurrent=self.mapIxAction[self.currentMapKey]+1 # current action index
# how many undo/redo actions are necessary?
howFar=0
self.affected=set()
if ixCurrent<ixGoto:
# redo actions; we move "whitespace item" behind the row which was clicked
howFar=ixGoto-ixCurrent
for ix in range(0,howFar):
self.redo(False)
elif ixCurrent>ixGoto:
# undo actions; we move "whitespace item" before the row which was clicked
howFar=ixCurrent-ixGoto
for ix in range(0,howFar):
self.undo(False)
else:
self.actionList.setEnabled(True)
QObject.disconnect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
self.actionList.setCurrentRow(curr)
QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
return
self.dbm.commit()
self.dbm.recacheAffectedNow(self.affected)
self.affected=set()
self.canvas.refresh()
if self.plugin.dockWidget:
lFeat=self.plugin.dockWidget.feature
lFeatType=self.plugin.dockWidget.featureType
self.plugin.dockWidget.loadFeature(lFeat,lFeatType)
self.actionList.setEnabled(True)
QObject.disconnect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
self.actionList.setCurrentRow(curr)
QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
def startAction(self,actNote):
"""Function remembers current state of system.
This function is called before performing an action that should be put into editing history.
It's expected that you call stopAction() function after edit actions finishes.
Then new record in history will be created.
@param actNote action description (in brief)
"""
if not self.dbm.currentKey:
# there is no current database
return
if self.actionInProgress:
print "failed! change in progress!"
return
self.actionInProgress=True
self.actStartId=self.dbm.getCurrentActionNumber()
self.actStopId=None
self.actNote=actNote
def stopAction(self,affected=set()):
"""Function is called after an edit action. It stores current state of system
and the state from last calling of startAction(). This two states of system
are considered new edit history record together and are stored in database.
@param affected list of all OSM features that was affected with just finished action"""
if not self.dbm.currentKey:
# there is no current database
return
if affected==None:
affected=set()
self.actStopId=self.dbm.getCurrentActionNumber()
if self.actStopId<=self.actStartId:
self.actionInProgress=False
return
for ix in range(len(self.mapActions[self.currentMapKey]),self.actionList.currentRow(),-1):
self.actionList.takeItem(ix)
self.redoCounter=self.redoCounter-1
ixAction=self.mapIxAction[self.currentMapKey]
cntActions=len(self.mapActions[self.currentMapKey])
if ixAction>-1:
fromId=self.mapActions[self.currentMapKey][ixAction][1]+1
self.dbm.removeChangeStepsBetween(fromId,self.actStartId)
del self.mapActions[self.currentMapKey][ixAction+1:cntActions]
if self.redoCounter==0:
self.redoButton.setEnabled(False)
self.plugin.dockWidget.redoButton.setEnabled(False)
self.mapIxAction[self.currentMapKey]=ixAction+1
ixAction=ixAction+1
# increase undo counter
self.undoCounter=self.undoCounter+1
self.undoButton.setEnabled(True)
self.plugin.dockWidget.undoButton.setEnabled(True)
self.mapActions[self.currentMapKey].append((self.actStartId,self.actStopId,self.actNote,affected))
self.actionList.addItem(self.actNote)
self.actionList.setCurrentRow(ixAction+1)
self.actStartId=self.actStopId=self.actNote=None
self.actionInProgress=False
def undo(self,standAlone=True):
"""Functions performs exactly one undo operation in system.
Last edit action is reverted, list of editing history on undo/redo dialog
shifts its current row up.
@param refresh if False, no canvas refresh will be performed after reverting an action; default is True
"""
self.undoButton.setEnabled(False)
self.plugin.dockWidget.undoButton.setEnabled(False)
self.redoButton.setEnabled(False)
self.plugin.dockWidget.redoButton.setEnabled(False)
(startId,stopId,note,affected)=self.mapActions[self.currentMapKey][self.mapIxAction[self.currentMapKey]]
# shift up in the list widget
ixCurrent=self.actionList.currentRow()
QObject.disconnect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
self.actionList.setCurrentRow(ixCurrent-1)
QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
self.mapIxAction[self.currentMapKey]=self.mapIxAction[self.currentMapKey]-1
changeSteps=self.dbm.getChangeSteps(startId,stopId)
for (change_type,tab_name,row_id,col_name,old_value,new_value) in changeSteps:
if change_type=='I':
self.dbm.setRowDeleted(tab_name,row_id)
elif change_type=='D':
self.dbm.setRowNotDeleted(tab_name,row_id)
elif change_type=='U':
self.dbm.setRowColumnValue(tab_name,col_name,old_value,row_id)
# increase redo counter
self.redoCounter=self.redoCounter+1
# decrease undo counter
self.undoCounter=self.undoCounter-1
# refresh
if standAlone:
self.dbm.commit()
self.dbm.recacheAffectedNow(affected)
self.canvas.refresh()
self.plugin.dockWidget.loadFeature(self.plugin.dockWidget.feature,self.plugin.dockWidget.featureType)
else:
self.affected.update(affected)
self.redoButton.setEnabled(True)
self.plugin.dockWidget.redoButton.setEnabled(True)
if self.undoCounter>0:
self.undoButton.setEnabled(True)
self.plugin.dockWidget.undoButton.setEnabled(True)
def redo(self,standAlone=True):
"""Functions performs exactly one redo operation in system.
Last reverted edit action is redone again. List of editing history on undo/redo dialog
shifts its current row down.
@param refresh if False, no canvas refresh will be performed after redo action; default is True
"""
self.undoButton.setEnabled(False)
self.plugin.dockWidget.undoButton.setEnabled(False)
self.redoButton.setEnabled(False)
self.plugin.dockWidget.redoButton.setEnabled(False)
(startId,stopId,note,affected)=self.mapActions[self.currentMapKey][self.mapIxAction[self.currentMapKey]+1]
# shift down in the list widget
ixCurrent=self.actionList.currentRow()
QObject.disconnect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
self.actionList.setCurrentRow(ixCurrent+1)
QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged)
self.mapIxAction[self.currentMapKey]=self.mapIxAction[self.currentMapKey]+1
if self.mapIxAction[self.currentMapKey]==len(self.mapActions[self.currentMapKey]):
self.redoButton.setEnabled(False)
self.plugin.dockWidget.redoButton.setEnabled(False)
changeSteps=self.dbm.getChangeSteps(startId,stopId)
for (change_type,tab_name,row_id,col_name,old_value,new_value) in changeSteps:
if change_type=='I':
self.dbm.setRowNotDeleted(tab_name,row_id)
elif change_type=='D':
self.dbm.setRowDeleted(tab_name,row_id)
elif change_type=='U':
self.dbm.setRowColumnValue(tab_name,col_name,new_value,row_id)
# decrease redo counter
self.redoCounter=self.redoCounter-1
# increase undo counter
self.undoCounter=self.undoCounter+1
# refresh
if standAlone:
self.dbm.commit()
self.dbm.recacheAffectedNow(affected)
self.canvas.refresh()
self.plugin.dockWidget.loadFeature(self.plugin.dockWidget.feature,self.plugin.dockWidget.featureType)
else:
self.affected.update(affected)
self.undoButton.setEnabled(True)
self.plugin.dockWidget.undoButton.setEnabled(True)
if self.redoCounter>0:
self.redoButton.setEnabled(True)
self.plugin.dockWidget.redoButton.setEnabled(True)