# -*- coding: utf-8 -*-
"""
Copyright (C) 2012 Oliver Tengler
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from typing import List, Set
from PyQt5.QtCore import Qt, QRect, QSize, pyqtSignal, pyqtSlot, QModelIndex, QObject
from PyQt5.QtGui import QFont, QPixmap, QStandardItemModel, QStandardItem, QPainter
from PyQt5.QtWidgets import QApplication, QStyledItemDelegate, QStyleOptionViewItem, QStyle, QDialog, QMessageBox, QWidget, QCheckBox, QListView
from fulltextindex.IndexConfiguration import IndexConfiguration, indexTypeInfo, IndexMode
from tools.Config import Config
import tools.FileTools as FileTools
from dialogs import StackTraceMessageBox
from dialogs.UserHintDialog import ButtonType,showUserHint
from .Ui_SettingsDialog import Ui_SettingsDialog
from .SettingsItem import SettingsItem
import AppConfig
userHintUpdateIndex = """
<p align='justify'>You added or changed indexed search locations:
%(locations)s
</p>
<p align='justify'>Do you want me to update the indexes now?</p>
<p align='jusitfy'> The update runs in the background and continues even if you close the program. During the update the index cannot be used.
To manually start the index update press the 'Update Index' button in the toolbar. See the help for more details.</p>
"""
class SettingsEditorDelegate(QStyledItemDelegate):
itemHeight = 40
def __init__(self, parent: QWidget=None) -> None:
super().__init__(parent)
if parent:
self.boldFont = QFont(parent.font())
else:
self.boldFont = QFont(QApplication.font())
self.boldFont.setBold(True)
self.defaultLocationRow = -1
self.defaultPixmap = QPixmap(":/default/resources/Default.png")
self.defaultPixmapSize = 20
def setDefaultLocationRow(self, row: int) -> None:
self.defaultLocationRow = row
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None:
QApplication.style().drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter, self.parent())
rect = QRect(option.rect)
rect.setHeight(self.itemHeight)
third = (rect.width()-12) / 3
rect1 = QRect(rect)
rect1.translate(12, 0)
rect1.setWidth(third-self.defaultPixmapSize)
rect2 = QRect(rect1)
rect2.translate(third+self.defaultPixmapSize, 0)
rect2.setWidth(third*2)
painter.save()
location = index.data(Qt.UserRole+1)
if location.directories:
dirs = self.tr("Directories: ") + location.directoriesAsString()
if location.extensions:
dirs += " ("
dirs += self.tr("Extensions: ") + location.extensionsAsString()
dirs += ")"
painter.drawText(rect2, Qt.AlignVCenter + Qt.TextWordWrap, dirs)
painter.setFont(self.boldFont)
locationName = location.displayName()
if index.row() == self.defaultLocationRow:
locationName = locationName + self.tr(" (Default)")
painter.drawPixmap(rect1.right(), rect1.center().y()-self.defaultPixmapSize/2, self.defaultPixmapSize, self.defaultPixmapSize, self.defaultPixmap)
painter.drawText(rect1, Qt.AlignVCenter + Qt.TextWordWrap, locationName)
painter.restore()
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
baseHint = super().sizeHint(option, index)
return QSize(baseHint.width(), self.itemHeight)
def setCheckBox(check: QCheckBox, value: bool) -> None:
if value:
check.setCheckState(Qt.Checked)
else:
check.setCheckState(Qt.Unchecked)
class LocationControl (QObject):
def __init__(self, settingsItem: SettingsItem, listView: QListView, searchLocations: List[IndexConfiguration], readOnly: bool) -> None:
super().__init__(None)
self.settingsItem = settingsItem
self.listView = listView
self.readOnly = readOnly
self.model = QStandardItemModel()
self.listView.setModel(self.model)
self.listView.setItemDelegate(SettingsEditorDelegate(self.listView))
self.listView.selectionModel().currentChanged.connect(self.selectionChanged)
self.settingsItem.dataChanged.connect(self.updateDisplayRole)
for location in searchLocations:
self.addLocation(location)
index = self.model.index(0, 0)
if index.isValid():
self.listView.setCurrentIndex(index)
else:
self.settingsItem.resetAndDisable()
@pyqtSlot()
def updateDisplayRole(self) -> None:
index = self.listView.currentIndex()
if index.isValid():
self.saveDataForItem(index)
@pyqtSlot(QModelIndex, QModelIndex)
def selectionChanged(self, current: QModelIndex, previous: QModelIndex) -> None:
self.saveDataForItem(previous)
self.loadDataFromItem(current)
def saveDataForItem(self, index: QModelIndex) -> None:
if not index.isValid():
return
editor = self.settingsItem
location = IndexConfiguration(editor.name(),
editor.extensions(),
editor.directories(),
editor.dirExcludes(),
editor.indexDB(),
editor.indexUpdateMode(),
editor.indexType())
self.model.setData(index, location, Qt.UserRole+1)
def loadDataFromItem(self, index: QModelIndex) -> None:
editor = self.settingsItem
if not index.isValid():
editor.resetAndDisable()
return
location = index.data(Qt.UserRole+1)
editor.setData(location.displayName(),
location.directoriesAsString(),
location.extensionsAsString(),
location.dirExcludesAsString(),
location.indexUpdateMode,
location.indexdb,
location.indexType)
editor.enable(not self.readOnly)
def addLocation(self, location: IndexConfiguration, activateLocation:bool=False) -> None:
rows = self.model.rowCount()
if self.model.insertRow(rows):
item = QStandardItem(location.displayName())
item.setData(location)
self.model.setItem(rows, item)
index = self.model.index(rows, 0)
if activateLocation and index.isValid():
self.listView.setCurrentIndex(index)
self.settingsItem.setFocus(Qt.ActiveWindowFocusReason)
self.settingsItem.nameSelectAll()
def locations(self) -> List[IndexConfiguration]:
locs = []
for row in range(self.model.rowCount()):
index = self.model.index(row, 0)
locs.append(index.data(Qt.UserRole+1))
return locs
class SettingsDialog (QDialog):
updateChangedIndexes = pyqtSignal(list)
def __init__ (self, parent: QWidget, searchLocations: List[IndexConfiguration], globalSearchLocations: List[IndexConfiguration], config: Config) -> None:
super ().__init__(parent)
self.ui = Ui_SettingsDialog()
self.ui.setupUi(self)
self.setProperty("shadeBackground", True) # fill background with gradient as defined in style sheet
self.searchLocations = searchLocations
self.ui.fontComboBox.setCurrentFont(QFont(config.SourceViewer.FontFamily))
self.ui.editFontSize.setText(str(config.SourceViewer.FontSize))
self.ui.editTabWidth.setText(str(config.SourceViewer.TabWidth))
self.ui.editPreviewLines.setText(str(config.previewLines))
setCheckBox (self.ui.checkActivateFirstMatch, config.activateFirstMatch)
setCheckBox (self.ui.checkMatchOverFiles, config.matchOverFiles)
setCheckBox (self.ui.checkConfirmClose, config.showCloseConfirmation)
setCheckBox (self.ui.checkShowMatchList, config.showMatchList)
setCheckBox (self.ui.checkShowLineNumbers, config.SourceViewer.showLineNumbers)
isDarkTheme = config.theme == AppConfig.darkTheme
setCheckBox (self.ui.checkDarkTheme, isDarkTheme)
self.myLocations = LocationControl(self.ui.settingsItem, self.ui.listViewLocations, searchLocations, False)
self.globalLocations = LocationControl(self.ui.globalSettingsItem, self.ui.listViewGlobalLocations, globalSearchLocations, True)
# Check which location is the current default location
defaultLocation = config.defaultLocation
for row, location in enumerate(searchLocations):
if defaultLocation == location.displayName():
self.__defaultLocationChanged (self.ui.listViewLocations, self.ui.listViewGlobalLocations, row)
for row, location in enumerate(globalSearchLocations):
if defaultLocation == location.displayName():
self.__defaultLocationChanged (self.ui.listViewGlobalLocations, self.ui.listViewLocations, row)
# If there are no search locations from the global config.txt then remove the corresponding tab
if not globalSearchLocations:
self.ui.tabWidget.removeTab(1)
del self.ui.tabGlobalSearchLocations
self.ui.settingsItem.setFocus(Qt.ActiveWindowFocusReason)
def defaultLocation (self) -> str:
index = None
row = self.ui.listViewLocations.itemDelegate().defaultLocationRow
if row != -1:
model = self.ui.listViewLocations.model()
index = model.index(row, 0)
row = self.ui.listViewGlobalLocations.itemDelegate().defaultLocationRow
if row != -1:
model = self.ui.listViewGlobalLocations.model()
index = model.index(row, 0)
if index and index.isValid():
location: IndexConfiguration = index.data(Qt.UserRole+1)
return location.displayName()
return ""
def locations(self) -> List[IndexConfiguration]:
return self.myLocations.locations()
def addExistingLocation(self, location: IndexConfiguration, activateLocation:bool=True) -> None:
self.myLocations.addLocation(location, activateLocation)
@pyqtSlot()
def addLocation (self) -> None:
location = IndexConfiguration(self.tr("New location"))
self.addExistingLocation(location, True)
@pyqtSlot()
def setDefaultLocation (self) -> None:
index = self.ui.listViewLocations.currentIndex ()
if index.isValid():
self.__defaultLocationChanged (self.ui.listViewLocations, self.ui.listViewGlobalLocations, index.row())
else:
self.__defaultLocationChanged (self.ui.listViewLocations, self.ui.listViewGlobalLocations, -1)
@pyqtSlot()
def setDefaultLocationGlobal (self) -> None:
index = self.ui.listViewGlobalLocations.currentIndex ()
if index.isValid():
self.__defaultLocationChanged (self.ui.listViewGlobalLocations, self.ui.listViewLocations, index.row())
else:
self.__defaultLocationChanged (self.ui.listViewGlobalLocations, self.ui.listViewLocations, -1)
def __defaultLocationChanged(self, listviewCurrent: QListView, listviewOther: QListView, newDefaultRow: int) -> None:
listviewCurrent.itemDelegate().setDefaultLocationRow(newDefaultRow)
listviewOther.itemDelegate().setDefaultLocationRow(-1)
# Refresh the listview
model = listviewCurrent.model()
firstIndex = model.index(0, 0)
lastIndex = model.index(model.rowCount()-1, 0)
if firstIndex.isValid() and lastIndex.isValid():
model.dataChanged.emit(firstIndex, lastIndex)
@pyqtSlot()
def duplicateLocation(self) -> None:
index = self.ui.listViewLocations.currentIndex ()
if index.isValid():
location = index.data(Qt.UserRole+1)
duplicated = IndexConfiguration (self.tr("Duplicated ") + location.displayName(),
location.extensionsAsString(),
location.directoriesAsString(),
location.dirExcludesAsString(),
"",
location.indexUpdateMode)
self.myLocations.addLocation(duplicated, True)
@pyqtSlot()
def removeLocation (self) -> None:
index = self.ui.listViewLocations.currentIndex ()
if index.isValid():
self.myLocations.model.removeRow(index.row())
@pyqtSlot()
def okClicked(self) -> None:
index = self.ui.listViewLocations.currentIndex()
if index.isValid():
self.myLocations.saveDataForItem (index)
# Check if there are search location with the same name and reject this
names: Set[str] = set()
for location in self.locations():
name = location.displayName().lower()
if name in names:
QMessageBox.warning(self,
self.tr("Duplicate search location names"),
self.tr("Please choose a unique name for each search location.") + " '" + location.displayName() + "' " + self.tr("is used more than once."),
QMessageBox.StandardButtons(QMessageBox.Ok))
return
names.add(name)
if self.__saveUserConfig():
self.accept()
else:
self.reject()
def __saveUserConfig (self) -> bool:
locations = self.locations()
config = Config (typeInfoFunc=AppConfig.configTypeInfo)
for location in locations:
locConf = Config(typeInfoFunc=indexTypeInfo)
locConf.indexName = location.indexName
locConf.extensions = location.extensionsAsString()
locConf.directories = location.directoriesAsString()
locConf.dirExcludes = location.dirExcludesAsString()
locConf.indexUpdateMode = location.indexUpdateMode
locConf.indexType = location.indexType
locConf.indexdb = location.indexdb
setattr(config, "Index_" + FileTools.removeInvalidFileChars(location.indexName), locConf)
config.sourceViewer.FontFamily = self.ui.fontComboBox.currentFont().family()
config.sourceViewer.FontSize = self.ui.editFontSize.text()
config.sourceViewer.TabWidth = self.ui.editTabWidth.text()
config.sourceViewer.showLineNumbers = self.ui.checkShowLineNumbers.checkState() == Qt.Checked
config.matchOverFiles = self.ui.checkMatchOverFiles.checkState() == Qt.Checked
config.activateFirstMatch = self.ui.checkActivateFirstMatch.checkState() == Qt.Checked
config.showCloseConfirmation = self.ui.checkConfirmClose.checkState() == Qt.Checked
config.showMatchList = self.ui.checkShowMatchList.checkState() == Qt.Checked
config.defaultLocation = self.defaultLocation()
config.previewLines = int(self.ui.editPreviewLines.text())
theme = ""
if self.ui.checkDarkTheme.checkState() == Qt.Checked:
theme = AppConfig.darkTheme
config.theme = theme
try:
AppConfig.saveUserConfig (config)
except:
self.failedToSaveUserConfigMessage()
return False
else:
updateDisplayNames = self.__getAddedOrChangedIndexedSearchLocations (self.searchLocations, locations)
if updateDisplayNames:
# Show a user hint which allows to update added or changed indexes<ul>
locationsHtml = "<ul>"
for displayName in updateDisplayNames:
locationsHtml += "<li>" + displayName + "</li>"
locationsHtml += "</ul>"
text = self.tr(userHintUpdateIndex) % {"locations" : locationsHtml}
result = showUserHint (self, "updateIndexes", self.tr("Update indexes"), text, ButtonType.Yes, False, ButtonType.No, True, bShowHintAgain=True)
if result == ButtonType.Yes:
self.updateChangedIndexes(updateDisplayNames)
return True
def __getAddedOrChangedIndexedSearchLocations (self, currentSearchLocations: List[IndexConfiguration],
newSearchLocations: List[IndexConfiguration]) -> List[str]:
"""Returns a list of display names of added or changed indexed search locations."""
changedLocations: List[str] = []
for location in newSearchLocations:
if location.indexUpdateMode == IndexMode.ManualIndexUpdate or location.indexUpdateMode == IndexMode.TriggeredIndexUpdate:
bFound = False
for oldLocation in currentSearchLocations:
if location == oldLocation:
bFound = True
break
if not bFound:
changedLocations.append(location.displayName())
return changedLocations
def failedToSaveUserConfigMessage(self) -> None:
StackTraceMessageBox.show(self,
self.tr("Failed to save user config"),
self.tr("The user config file could not be saved to the user profile"))
def main() -> None:
import sys
app = QApplication(sys.argv)
locations = [IndexConfiguration("Qt Source", "h,cpp", "D:\\qt", "","D:\\index.dat"), IndexConfiguration("Linux", "", "", "", "")]
conf = Config()
conf.sourceViewer = Config()
conf.sourceViewer.tabwidth = 4
conf.sourceViewer.fontfamily = "Courier"
conf.sourceViewer.fontsize = 10
conf.matchOverFiles=True
conf.showCloseConfirmation=True
w = SettingsDialog(None,locations, locations, conf)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()