"""@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("")) 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("")) 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("")) 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("")) 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 ixCurrentixGoto: # 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)