def __init__(self, controller):
     """
         Default constructor.
         'controller' - an AbstractTableController object
     """
     if not isinstance(controller, AbstractTableController):
         raise TypeError("Controller must be an 'AbstractTableController' type", 
                         {"raisingObject": self, "controller": controller})
     QtGui.QMainWindow.__init__(self)
     self.rowsPerPage = 100
     self._controller = controller
     self._ui = Ui_MainWindow()
     self._ui.setupUi(self)
     self._visibleRowsRanges = RangesList(0, 0)
     self._searchDialog = SearchDialog(self)
     self._searchResultsDialog = SearchResultsDialog(self)
     self._modifyingFieldDialog = ModifyingFieldDialog(self)
     self._setFileAndTableOperationsEnabled(False)
     QtCore.QObject.connect(self._ui.actionOpen, QtCore.SIGNAL("triggered()"), self.openFileAction)
     QtCore.QObject.connect(self._ui.actionSave, QtCore.SIGNAL("triggered()"), self.saveFileAction)
     QtCore.QObject.connect(self._ui.actionClose, QtCore.SIGNAL("triggered()"), self.closeFileAction)
     QtCore.QObject.connect(self._ui.pagesTabs, QtCore.SIGNAL("currentChanged(int)"), self.activeTabChanged)
     QtCore.QObject.connect(self._ui.actionInsert, QtCore.SIGNAL("triggered()"), self.insertNewRowAction)
     QtCore.QObject.connect(self._ui.actionRemove, QtCore.SIGNAL("triggered()"), self.removeRowAction)
     QtCore.QObject.connect(self._ui.actionSearch, QtCore.SIGNAL("triggered()"), self.searchRowAction)
     QtCore.QObject.connect(self._ui.tableView, QtCore.SIGNAL("doubleClicked(const QModelIndex &)"), self.tableViewDbClickEvent)
 def __init__(self, filePath = None):
     """
         Default constructor. Opens specified file for reading and writing.
         'filePath' - path to the file. If file doesn't exist, new file created.
     If 'filePath' has None value, a temporary file created, which would be 
     removed after it's closing.
     """
     QtCore.QObject.__init__(self)
     self._isTemporary = False;
     if filePath == None:
         filePath = os.tempnam()
         self._isTemporary = True
     self._file = QtCore.QFile(filePath)
     if not self._file.open(QtCore.QIODevice.ReadWrite):
         raise IOError("File can't be opened!",
                       {"raisingObject": self, "path": filePath, "error": self._file.error()})
     self._validRanges = RangesList(0, self._file.size())
     self._packingListeners = []
 def setUp(self):
     self._list = RangesList(0, 0)
class RangesList_test(unittest.TestCase):
    def setUp(self):
        self._list = RangesList(0, 0)

    def tearDown(self):
        self._list.clear()

    def testUnion(self):
        self._list.add(1, 4)
        self._list.add(2, 5)
        self._list.add(0, 2)
        self._list.add(5, 7)
        self.assertEqual(self._list.size(), 1, ".size() error")
        self.assertEqual(self._list.ranges(), [(0, 7)], "Ranges union result error")
        self._list.add(8, 12)
        self._list.add(14, 16)
        self._list.add(15, 17)
        self._list.add(19, 24)
        self.assertEqual(self._list.size(), 4, ".size() error")
        self.assertEqual(self._list.ranges(), [(0, 7), (8, 12), (14, 17), (19, 24)], "Ranges union result error")

    def testSpliting(self):
        self._list.add(0, 7)
        self._list.add(8, 12)
        self._list.add(14, 17)
        self._list.add(19, 24)
        self._list.remove(2, 5)
        self._list.remove(6, 13)
        self._list.remove(14, 24)
        self.assertEqual(self._list.size(), 2, ".size() error")
        self.assertEquals(self._list.ranges(), [(0, 2), (5, 6)], "Ranges splitting result error")
class AutoResizeableFile(QtCore.QObject):
    """
        Class representing the auto-resizeable files
    """
    def __init__(self, filePath = None):
        """
            Default constructor. Opens specified file for reading and writing.
            'filePath' - path to the file. If file doesn't exist, new file created.
        If 'filePath' has None value, a temporary file created, which would be 
        removed after it's closing.
        """
        QtCore.QObject.__init__(self)
        self._isTemporary = False;
        if filePath == None:
            filePath = os.tempnam()
            self._isTemporary = True
        self._file = QtCore.QFile(filePath)
        if not self._file.open(QtCore.QIODevice.ReadWrite):
            raise IOError("File can't be opened!",
                          {"raisingObject": self, "path": filePath, "error": self._file.error()})
        self._validRanges = RangesList(0, self._file.size())
        self._packingListeners = []
        
        
    def __del__(self):
        """
            Default finalizing method (destructor).
        Packs, saves all changes and closes the file
        """
        self.close()
               
                  
    def close(self):
        """
            Packs, saves all changes and closes the file.
        Returns None.
        """
        if self._file.isOpen():
            self.flush()
            self._file.close()
            if self._isTemporary:
                self._file.remove()
                
                
    def path(self):
        """
            Returns file path
        """
        return self._file.fileName()
                        
                          
    def position(self):
        """
            Returns current file position
        """
        return self._file.pos()
    
    
    def size(self):
        """
            Returns real physical size of the file. 
        """
        return self._file.size()
    
    def validRanges(self):
        """
            Returns a list (each item - a 2-integer tuple) of valid file sections.
        Valid section is a one, which wasn't removed previously
        """
        return self._validRanges.ranges()
    
    
    def isOpened(self):
        """
            Returns True if file is opened, False otherwise
        """
        return self._file.isOpen()
    
    
    def endOfFile(self):
        """
            Returns True if current file position points to the end of physical file.
        """
        return self.position() >= self.size()
    
    
    def setPosition(self, position):
        """
            Sets new file position. Returns real value of position. 
            'position' - new file position (integer)
        """
        position = self._validateInteger(position)
        if not self._file.seek(position):
            raise IOError("Seeking position failed", 
                             {"raiseObject": self, "position": position, "error": self._file.error()})
        return self.position()
    
    
    def erase(self):
        """
            Erases all file content. Returns None
        """
        self._file.resize(0)
        self.setPosition(0)
        self._validRanges.clear()
    
    
    def addPackingListener(self, listener):
        """
            Adds a new file packing event listener. This event happens
        when all previously removed file sections are removed physically
        from the disk. Returns None
            'listener' - a FilePackingListener object reference
        """
        if not isinstance(listener, FilePackingListener):
            raise TypeError("Listener must has a 'FilePackingListener' type!",
                            {"raisingObject": self})
        self._packingListeners.append(listener)
        
        
    def removePackingListener(self, listener):
        """
            Removes listener added via 'addPackingListener' method.
            'listener' - a FilePackingListener object reference
        """
        if not isinstance(listener, FilePackingListener):
            raise TypeError("Listener must has a 'FilePackingListener' type!",
                            {"raisingObject": self})
        if not (listener in self._packingListeners):
            return
        del self._packingListeners[self._packingListeners.index(listener)]
        
        
    def realPositionFor(self, validPosition):
        """
            Translates any file position to real position in the physical
        file. This values may be different if any sections were removed 
        previously 
            'validPosition' - position to translate (integer)
        """
        ranges = self._validRanges.ranges()
        lastRangeIndex, currentSum = 0, 0
        rangeFound = False
        for lastRangeIndex in xrange(0, len(ranges)):
            currentRange = ranges[lastRangeIndex]
            if currentSum <= validPosition < currentSum + (currentRange[1] - currentRange[0]):
                rangeFound = True
                break
            else:
                currentSum += (currentRange[1] - currentRange[0])
        if not rangeFound:
            if currentSum == validPosition:
                return  validPosition 
            else:
                raise IndexError("Position is too large", {"raisingObject": self, 
                                                           "value": validPosition})
        lastEnumeratedRange = ranges[lastRangeIndex]
        neededDelta = validPosition - currentSum
        return lastEnumeratedRange[0] + neededDelta
    
        
    def readLine(self):
        """
            Reads and returns a line.
        """
        data = str(self._file.readLine())
        return data.strip()
    
    
    def read(self, size = -1):
        """
            Reads and returns a string with specified length
            'size' - needed length of string. If 'size' has -1 value,
        the whole file up to the end would be read
        """
        size = self._validateInteger(size)
        if size == -1:
            size = self._file.size() - self.position()   
        data = str(self._file.read(size))
        return data
    
    
    def readAll(self):
        """
            Reads and returns the whole file content, saves old filed position
        """
        oldPosition = self.position()
        self.setPosition(0)
        data = self.read()
        self.setPosition(oldPosition)
        return data
    
    
    def readInteger(self):
        """
            Reads next 4 bytes from the file and converts them to integer value,
        that would be returned 
        """
        oldPosition = self.position()
        string = self.read(4)
        if len(string) != 4:
            self._file.seek(oldPosition)
            raise EOFError("Can't read needed amount of bytes!",
                           {"raiseObject": self, "position": oldPosition, "fileSize": self._file.size()})
        return struct.unpack("i", string)[0]
    
    
    def write(self, string, eraseFirstSymbols = 0):
        """
            Writes data represented by 'string', previously removed
        'eraseFirstSymbols' bytes. Returns really written bytes amount
        """
        eraseFirstSymbols = self._validateInteger(eraseFirstSymbols)
        string = str(string)
        stringLength = len(string)
        eraseFirstSymbols = min(eraseFirstSymbols, self._file.size() - self.position())
        additionalSpace = stringLength - eraseFirstSymbols
        start= self.position() + eraseFirstSymbols
        newStart = start + additionalSpace
        oldPosition = self.position()
        self._shift(start, -1, newStart)
        written = self._file.writeData(string)
        self.setPosition(oldPosition + written)
        rangeToAdd = (oldPosition, oldPosition + stringLength)
        shiftingValue = newStart - start
        self._shiftValidRanges(start, shiftingValue)
        self._validRanges.add(rangeToAdd[0], rangeToAdd[1])
        return written
        
        
    def writeLine(self, string, eraseFirstSymbols = 0):
        """
            Writes a line represented by 'string', previously removed
        'eraseFirstSymbols' bytes. Returns really written bytes amount
        """
        string = str(string) + '\n'
        return self.write(string, eraseFirstSymbols)
        
    
    def writeInteger(self, value, eraseOld = False):
        """
            Writes an integer represented by 'value' with erasing
        previous value if 'eraseOld' is True. Returns really written bytes amount
        """
        value = self._validateInteger(value)
        eraseFirstSymbols = 0
        if eraseOld:
            eraseFirstSymbols = 4
        return self.write(struct.pack("i", value), eraseFirstSymbols)
    
    
    def flush(self):
        """
            Flushes file to the disk with previously packing.
        Returns None.
        """
        self.pack()
        self._file.flush()
        
        
    def pack(self):
        """
            Performs packing operation, that physically removes
        all removed previously file sections via 'removeRange' method.
        Returns None.
        """
        if self._validRanges.size() == 0:
            self.setPosition(0)
            self._file.resize(0)
            return
        shiftingInfos = []
        currentValidSize = 0
        totalShifting = 0
        for validRange in self._validRanges.ranges():
            shiftValue = currentValidSize - validRange[0]
            totalShifting += shiftValue
            if (validRange[0] != validRange[1]) and (totalShifting != 0):
                shiftingInfos.append(RangeShiftingInfo(validRange, totalShifting))
            self._shift(validRange[0], validRange[1], currentValidSize)
            currentValidSize += validRange[1] - validRange[0]
        self._file.resize(currentValidSize)
        if len(shiftingInfos) != 0:
            for listener in self._packingListeners:
                listener.filePackedEvent(shiftingInfos)
        self._validRanges.clear()
        self._validRanges.add(0, currentValidSize)
                        
    
    def removeRange(self, startPosition, endPosition):
        """
            Removes file section, specified by 'startPosition' and
        'endPosition' constrains ('endPosition' - excludly).Returns None
        """
        self._validRanges.remove(startPosition, endPosition)
        invalidSpacePercentage = (1 - (float(self._validRanges.size()) / float(self._file.size()))) * 100.0
        if invalidSpacePercentage > 25.0 or self._validRanges.size() >= 64:
            self.pack() 
        
    
    def _shift(self, start, end, newStart):
        if newStart == start:
            return
        if end == -1:
            end = self._file.size();
        start, end = min(start, end), max(start, end)
        oldPosition = self.position()
        self.setPosition(start)
        data = self.read(end - start)
        if newStart > self._file.size():
            self._file.resize(newStart)
        self.setPosition(newStart)
        self._file.writeData(data)
        self.setPosition(oldPosition)
        
    
    def _shiftValidRanges(self, fileStartPosition, shiftingValue):
        ranges = self._validRanges.ranges()
        firstShiftingRangeIndex = -1
        for i in xrange(0, len(ranges)):
            if ranges[i][0] <= fileStartPosition <= ranges[i][1]:
                firstShiftingRangeIndex = i
                break
        if firstShiftingRangeIndex == -1:
            return
        else:
            firstShiftingRange = ranges[firstShiftingRangeIndex]
            splitedRanges = [(firstShiftingRange[0], fileStartPosition), 
                             (fileStartPosition, firstShiftingRange[1])]
            self._validRanges.remove(splitedRanges[1][0], ranges[len(ranges) - 1][1])
            shiftingRanges = [(splitedRanges[1][0], splitedRanges[1][1])]
            for i in xrange(firstShiftingRangeIndex + 1, len(ranges)):
                if ranges[i][0] != ranges[i][1]:
                    shiftingRanges.append(ranges[i])
            shiftingInfos = []
            for item in shiftingRanges:
                self._validRanges.add(item[0] + shiftingValue, item[1] + shiftingValue)
                if (item[0] != item[1]) and (shiftingValue != 0):
                    shiftingInfos.append(RangeShiftingInfo(item, shiftingValue))
            if len(shiftingInfos) != 0:
                for listener in self._packingListeners:
                    listener.filePackedEvent(shiftingInfos)
                    
    
    def _validateInteger(self, value):
        try:
            return int(value)
        except (TypeError, AttributeError, ValueError):
            raise TypeError("Value mast be an integer", {"raisingObject": self, "value": value})
            
            
class MainWindow(QtGui.QMainWindow):
    """
        Main window class
    """
    def __init__(self, controller):
        """
            Default constructor.
            'controller' - an AbstractTableController object
        """
        if not isinstance(controller, AbstractTableController):
            raise TypeError("Controller must be an 'AbstractTableController' type", 
                            {"raisingObject": self, "controller": controller})
        QtGui.QMainWindow.__init__(self)
        self.rowsPerPage = 100
        self._controller = controller
        self._ui = Ui_MainWindow()
        self._ui.setupUi(self)
        self._visibleRowsRanges = RangesList(0, 0)
        self._searchDialog = SearchDialog(self)
        self._searchResultsDialog = SearchResultsDialog(self)
        self._modifyingFieldDialog = ModifyingFieldDialog(self)
        self._setFileAndTableOperationsEnabled(False)
        QtCore.QObject.connect(self._ui.actionOpen, QtCore.SIGNAL("triggered()"), self.openFileAction)
        QtCore.QObject.connect(self._ui.actionSave, QtCore.SIGNAL("triggered()"), self.saveFileAction)
        QtCore.QObject.connect(self._ui.actionClose, QtCore.SIGNAL("triggered()"), self.closeFileAction)
        QtCore.QObject.connect(self._ui.pagesTabs, QtCore.SIGNAL("currentChanged(int)"), self.activeTabChanged)
        QtCore.QObject.connect(self._ui.actionInsert, QtCore.SIGNAL("triggered()"), self.insertNewRowAction)
        QtCore.QObject.connect(self._ui.actionRemove, QtCore.SIGNAL("triggered()"), self.removeRowAction)
        QtCore.QObject.connect(self._ui.actionSearch, QtCore.SIGNAL("triggered()"), self.searchRowAction)
        QtCore.QObject.connect(self._ui.tableView, QtCore.SIGNAL("doubleClicked(const QModelIndex &)"), self.tableViewDbClickEvent)
        
        
    def openFileAction(self):
        """
            Slot for File->Open item triggered() signal
        Returns None
        """
        filePath = QtGui.QFileDialog.getOpenFileName(parent = self, caption = QtCore.QString("Open table..."),
                                          filter = QtCore.QString("CSV files (*.csv)"))
        if filePath == None or len(filePath) == 0:
            return
        result = self._controller.openTable(filePath)
        if not result.isOk():
            QtGui.QMessageBox.about(self, "Error...", result.message())
            return
        model = result.data()
        self._ui.tableView.setModel(model)
        self._visibleRowsRanges.clear()
        self._visibleRowsRanges.add(0, model.rowCount())
        self._distributeToPages()
        self._selectPage(0)
        self._setFileAndTableOperationsEnabled(True)
        
        
    def saveFileAction(self):
        """
            Slot for File->Save item triggered() signal
        Returns None
        """
        self._controller.save()
        
        
    def closeFileAction(self):
        """
            Slot for File->Close item triggered() signal
        Returns None
        """
        self._controller.save()
        self._controller.close()
        self._ui.pagesTabs.clear()
        self._ui.tableView.setModel(None)
        
        
    def activeTabChanged(self, index):
        """
            Slot for File->Open item triggered() signal
        Returns None
        """
        self._selectPage(index)
        
        
    def insertNewRowAction(self):
        """
            Slot for pages QTabView object currentChanged(int) signal
        Returns None
        """
        result = self._controller.insertRow(self._ui.tableView.currentIndex().row())
        if not result.isOk():
            QtGui.QMessageBox.about(self, "Error...", result.message())
            return
        index = self._visibleRowsRanges.ranges()[0][1]
        self._visibleRowsRanges.add(index, index + 1)
        self._distributeToPages()
        self._selectPage(self._ui.pagesTabs.currentIndex())
        
        
    def removeRowAction(self):
        """
            Slot for Table->Remove row item triggered() signal
        Returns None
        """
        result = self._controller.removeRow(self._ui.tableView.currentIndex().row())
        if not result.isOk():
            QtGui.QMessageBox.about(self, "Error...", result.message())
            return
        self._distributeToPages()
        self._selectPage(self._ui.pagesTabs.currentIndex())
        
    
    def tableViewDbClickEvent(self, ARG):
        """
            Slot for QTableView object doubleClicked(const QModelIndex &) signal
        Returns None
        """
        self._modifyingFieldDialog.show()
        self._modifyingFieldDialog.focusToEdit()
    
        
    def searchRowAction(self):
        """
            Slot for Table->Search for... item triggered() signal
        Returns None
        """
        self._searchDialog.show()
        
        
    def searchFor(self, columnKey, key):
        """
            Called by SearchDialog object for searching data.
        Returns None
            'keyColumn' - zero-based column index
            'key' - string-convertable object to search
        """
        result = self._controller.searchRows(columnKey, key)
        if not result.isOk():
            QtGui.QMessageBox.about(self, "Error...", result.message())
            return
        self._searchResultsDialog.showResults(result.data())
        
        
    def modifyCurrentField(self, value):
        """
            Called by ModifyingFieldDialog object for changing
        currently selected field. Returns None
            'value' - new field value
        """
        index = self._ui.tableView.currentIndex()
        result = self._controller.modifyField(index.row(), index.column(), value)
        if not result.isOk():
            QtGui.QMessageBox.about(self, "Error...", result.message())
            return
        newRow = index.row()
        newColumn = (index.column() + 1) % self.currentModel().columnCount()
        if newColumn == 0:
            newRow = (index.row() + 1) % self.currentModel().rowCount()
            if newRow == 0:
                newRow = self.currentModel().rowCount() - 1
        self._ui.tableView.setCurrentIndex(self.currentModel().createIndex(newRow, newColumn))       
        
        
    def currentModel(self):
        """
            Returns currently associated with controller model
        """
        if self._controller == None:
            return None
        return self._controller.model()
        
        
    def _distributeToPages(self):
        model = self._controller.model()
        pages = (model.rowCount() + self.rowsPerPage - 1) / self.rowsPerPage
        for i in xrange(pages, self._ui.pagesTabs.count()):
            self._ui.pagesTabs.removeTab(i)
        for i in xrange(self._ui.pagesTabs.count(), pages):
            tab = QtGui.QWidget()
            tab.setObjectName(QtCore.QString("Tab_" + str(i + 1))) 
            self._ui.pagesTabs.addTab(tab, QtCore.QString(str(i + 1)))
            
            
    def _selectPage(self, page):
        model = self._controller.model()
        if model == None:
            return
        rangeToShow = (page * self.rowsPerPage, min((page + 1) * self.rowsPerPage, model.rowCount())) 
        for item in self._visibleRowsRanges.ranges():
            for i in xrange(item[0], item[1]):
                if i < rangeToShow[0] or i >= rangeToShow[1]:
                    self._ui.tableView.hideRow(i)
        self._visibleRowsRanges.clear()
        for i in xrange(rangeToShow[0], rangeToShow[1]):
            self._ui.tableView.showRow(i)
        self._visibleRowsRanges.add(rangeToShow[0], rangeToShow[1])
        self._ui.tableView.setCurrentIndex(self._controller.model().createIndex(rangeToShow[0], 0))
        
        
    def _setFileAndTableOperationsEnabled(self, enabled):
        self._ui.actionClose.setEnabled(enabled)
        self._ui.actionSave.setEnabled(enabled)
        self._ui.actionRemove.setEnabled(enabled)
        self._ui.actionSearch.setEnabled(enabled)
        self._ui.actionInsert.setEnabled(enabled)
        if not enabled:
            self._searchDialog.hide()
            self._searchResultsDialog.hide()
            self._modifyingFieldDialog.hide()