diff --git a/manuskript/ui/search.py b/manuskript/ui/search.py
index 06441ae..f177c12 100644
--- a/manuskript/ui/search.py
+++ b/manuskript/ui/search.py
@@ -1,147 +1,151 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
-from PyQt5.QtCore import Qt, QRect
-from PyQt5.QtGui import QPalette, QFontMetrics
-from PyQt5.QtWidgets import QWidget, QMenu, QAction, qApp, QListWidgetItem, QStyledItemDelegate, QStyle
+from PyQt5.QtCore import Qt, QRect, QEvent, QCoreApplication
+from PyQt5.QtGui import QPalette, QFontMetrics, QKeySequence
+from PyQt5.QtWidgets import QWidget, qApp, QListWidgetItem, QStyledItemDelegate, QStyle, QLabel, QToolTip, QShortcut
+
-from manuskript.enums import Outline
from manuskript.functions import mainWindow
from manuskript.ui import style
from manuskript.ui.search_ui import Ui_search
-from manuskript.models import references as Ref
+from manuskript.enums import Model
+
+from manuskript.models.flatDataModelWrapper import flatDataModelWrapper
+from manuskript.ui.searchMenu import searchMenu
+from manuskript.ui.highlighters.searchResultHighlighters.searchResultHighlighter import searchResultHighlighter
class search(QWidget, Ui_search):
def __init__(self, parent=None):
+ _translate = QCoreApplication.translate
+
QWidget.__init__(self, parent)
self.setupUi(self)
- self.options = {
- "All": True,
- "Title": True,
- "Text": True,
- "Summary": False,
- "Notes": False,
- "POV": False,
- "Status": False,
- "Label": False,
- "CS": True
- }
+ self.searchTextInput.returnPressed.connect(self.search)
- self.text.returnPressed.connect(self.search)
- self.generateOptionMenu()
+ self.searchMenu = searchMenu()
+ self.btnOptions.setMenu(self.searchMenu)
self.delegate = listResultDelegate(self)
self.result.setItemDelegate(self.delegate)
+ self.result.setMouseTracking(True)
self.result.itemClicked.connect(self.openItem)
self.result.setStyleSheet(style.searchResultSS())
- self.text.setStyleSheet(style.lineEditSS())
+ self.searchTextInput.setStyleSheet(style.lineEditSS())
- def generateOptionMenu(self):
- self.menu = QMenu(self)
- a = QAction(self.tr("Search in:"), self.menu)
- a.setEnabled(False)
- self.menu.addAction(a)
- for i, d in [
- (self.tr("All"), "All"),
- (self.tr("Title"), "Title"),
- (self.tr("Text"), "Text"),
- (self.tr("Summary"), "Summary"),
- (self.tr("Notes"), "Notes"),
- (self.tr("POV"), "POV"),
- (self.tr("Status"), "Status"),
- (self.tr("Label"), "Label"),
- ]:
- a = QAction(i, self.menu)
- a.setCheckable(True)
- a.setChecked(self.options[d])
- a.setData(d)
- a.triggered.connect(self.updateOptions)
- self.menu.addAction(a)
- self.menu.addSeparator()
+ self.searchResultHighlighter = searchResultHighlighter()
- a = QAction(self.tr("Options:"), self.menu)
- a.setEnabled(False)
- self.menu.addAction(a)
- for i, d in [
- (self.tr("Case sensitive"), "CS"),
- ]:
- a = QAction(i, self.menu)
- a.setCheckable(True)
- a.setChecked(self.options[d])
- a.setData(d)
- a.triggered.connect(self.updateOptions)
- self.menu.addAction(a)
- self.menu.addSeparator()
+ self.noResultsLabel = QLabel(_translate("Search", "No results found"), self.result)
+ self.noResultsLabel.setVisible(False)
+ self.noResultsLabel.setStyleSheet("QLabel {color: gray;}")
- self.btnOptions.setMenu(self.menu)
+ # Add shortcuts for navigating through search results
+ QShortcut(QKeySequence(_translate("MainWindow", "F3")), self.searchTextInput, self.nextSearchResult)
+ QShortcut(QKeySequence(_translate("MainWindow", "Shift+F3")), self.searchTextInput, self.previousSearchResult)
- def updateOptions(self):
- a = self.sender()
- self.options[a.data()] = a.isChecked()
+ # These texts are already included in translation files but including ":" at the end. We force here the
+ # translation for them without ":"
+ _translate("MainWindow", "Situation")
+ _translate("MainWindow", "Status")
+
+ def nextSearchResult(self):
+ if self.result.currentRow() < self.result.count() - 1:
+ self.result.setCurrentRow(self.result.currentRow() + 1)
+ else:
+ self.result.setCurrentRow(0)
+
+ if 0 < self.result.currentRow() < self.result.count():
+ self.openItem(self.result.currentItem())
+
+ def previousSearchResult(self):
+ if self.result.currentRow() > 0:
+ self.result.setCurrentRow(self.result.currentRow() - 1)
+ else:
+ self.result.setCurrentRow(self.result.count() - 1)
+
+ if 0 < self.result.currentRow() < self.result.count():
+ self.openItem(self.result.currentItem())
+
+ def prepareRegex(self, searchText):
+ import re
+
+ flags = re.UNICODE
+
+ if self.searchMenu.caseSensitive() is False:
+ flags |= re.IGNORECASE
+
+ if self.searchMenu.regex() is False:
+ searchText = re.escape(searchText)
+
+ if self.searchMenu.matchWords() is True:
+ # Source: https://stackoverflow.com/a/15863102
+ searchText = r'\b' + searchText + r'\b'
+
+ return re.compile(searchText, flags)
def search(self):
- text = self.text.text()
-
- # Choosing the right columns
- lstColumns = [
- ("Title", Outline.title),
- ("Text", Outline.text),
- ("Summary", Outline.summarySentence),
- ("Summary", Outline.summaryFull),
- ("Notes", Outline.notes),
- ("POV", Outline.POV),
- ("Status", Outline.status),
- ("Label", Outline.label),
- ]
- columns = [c[1] for c in lstColumns if self.options[c[0]] or self.options["All"]]
-
- # Setting override cursor
- qApp.setOverrideCursor(Qt.WaitCursor)
-
- # Searching
- model = mainWindow().mdlOutline
- results = model.findItemsContaining(text, columns, self.options["CS"])
-
- # Showing results
self.result.clear()
- for r in results:
- index = model.getIndexByID(r)
- if not index.isValid():
- continue
- item = index.internalPointer()
- i = QListWidgetItem(item.title(), self.result)
- i.setData(Qt.UserRole, r)
- i.setData(Qt.UserRole + 1, item.path())
- self.result.addItem(i)
+ self.result.setCurrentRow(0)
- # Removing override cursor
- qApp.restoreOverrideCursor()
+ searchText = self.searchTextInput.text()
+ if len(searchText) > 0:
+ searchRegex = self.prepareRegex(searchText)
+ results = []
+
+ # Set override cursor
+ qApp.setOverrideCursor(Qt.WaitCursor)
+
+ for model, modelName in [
+ (mainWindow().mdlOutline, Model.Outline),
+ (mainWindow().mdlCharacter, Model.Character),
+ (flatDataModelWrapper(mainWindow().mdlFlatData), Model.FlatData),
+ (mainWindow().mdlWorld, Model.World),
+ (mainWindow().mdlPlots, Model.Plot)
+ ]:
+ filteredColumns = self.searchMenu.columns(modelName)
+
+ # Searching
+ if len(filteredColumns):
+ results += model.searchOccurrences(searchRegex, filteredColumns)
+
+ # Showing results
+ self.generateResultsLists(results)
+
+ # Remove override cursor
+ qApp.restoreOverrideCursor()
+
+ def generateResultsLists(self, results):
+ self.noResultsLabel.setVisible(len(results) == 0)
+ for result in results:
+ item = QListWidgetItem(result.title(), self.result)
+ item.setData(Qt.UserRole, result)
+ item.setData(Qt.UserRole + 1, ' > '.join(result.path()))
+ item.setData(Qt.UserRole + 2, result.context())
+ self.result.addItem(item)
def openItem(self, item):
- r = Ref.textReference(item.data(Qt.UserRole))
- Ref.open(r)
- # mw = mainWindow()
- # index = mw.mdlOutline.getIndexByID(item.data(Qt.UserRole))
- # mw.mainEditor.setCurrentModelIndex(index, newTab=True)
+ self.searchResultHighlighter.highlightSearchResult(item.data(Qt.UserRole))
+ def leaveEvent(self, event):
+ self.delegate.mouseLeave()
class listResultDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
QStyledItemDelegate.__init__(self, parent)
+ self._tooltipRowIndex = -1
def paint(self, painter, option, index):
extra = index.data(Qt.UserRole + 1)
+
if not extra:
return QStyledItemDelegate.paint(self, painter, option, index)
-
else:
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.color(QPalette.Highlight))
title = index.data()
- extra = " - {}".format(extra)
painter.drawText(option.rect.adjusted(2, 1, 0, 0), Qt.AlignLeft, title)
fm = QFontMetrics(option.font)
@@ -153,5 +157,18 @@ class listResultDelegate(QStyledItemDelegate):
painter.setPen(Qt.white)
else:
painter.setPen(Qt.gray)
- painter.drawText(r.adjusted(2, 1, 0, 0), Qt.AlignLeft, extra)
+ painter.drawText(r.adjusted(2, 1, 0, 0), Qt.AlignLeft, " - {}".format(extra))
painter.restore()
+
+ def editorEvent(self, event, model, option, index):
+ if event.type() == QEvent.MouseMove and self._tooltipRowIndex != index.row():
+ self._tooltipRowIndex = index.row()
+ context = index.data(Qt.UserRole + 2)
+ extra = index.data(Qt.UserRole + 1)
+ QToolTip.showText(event.globalPos(),
+ "#" + str(index.row()) + " - " + extra + "
" + context + "
")
+ return True
+ return False
+
+ def mouseLeave(self):
+ self._tooltipRowIndex = -1
diff --git a/manuskript/ui/searchMenu.py b/manuskript/ui/searchMenu.py
new file mode 100644
index 0000000..59468f8
--- /dev/null
+++ b/manuskript/ui/searchMenu.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# --!-- coding: utf8 --!--
+from PyQt5.QtWidgets import QMenu, QAction
+from PyQt5.QtCore import QCoreApplication
+from PyQt5 import QtCore
+
+from manuskript.searchLabels import OutlineSearchLabels, CharacterSearchLabels, FlatDataSearchLabels, WorldSearchLabels, PlotSearchLabels
+from manuskript.models.searchFilter import searchFilter
+from manuskript.enums import Model
+
+
+def filterKey(modelPreffix, column):
+ return modelPreffix + str(column)
+
+
+class searchMenu(QMenu):
+ def __init__(self, parent=None):
+ QMenu.__init__(self, parent)
+
+ _translate = QCoreApplication.translate
+ # Model keys must match the ones used in search widget class
+ self.filters = {
+ Model.Outline: searchFilter(_translate("MainWindow", "Outline"), True, list(OutlineSearchLabels.keys())),
+ Model.Character: searchFilter(_translate("MainWindow", "Characters"), True, list(CharacterSearchLabels.keys())),
+ Model.FlatData: searchFilter(_translate("MainWindow", "FlatData"), True, list(FlatDataSearchLabels.keys())),
+ Model.World: searchFilter(_translate("MainWindow", "World"), True, list(WorldSearchLabels.keys())),
+ Model.Plot: searchFilter(_translate("MainWindow", "Plot"), True, list(PlotSearchLabels.keys()))
+ }
+
+ self.options = {
+ "CS": [self.tr("Case sensitive"), True],
+ "MatchWords": [self.tr("Match words"), False],
+ "Regex": [self.tr("Regex"), False]
+ }
+
+ self._generateOptions()
+
+ def _generateOptions(self):
+ a = QAction(self.tr("Search in:"), self)
+ a.setEnabled(False)
+ self.addAction(a)
+ for filterKey in self.filters:
+ a = QAction(self.tr(self.filters[filterKey].label()), self)
+ a.setCheckable(True)
+ a.setChecked(self.filters[filterKey].enabled())
+ a.setData(filterKey)
+ a.triggered.connect(self._updateFilters)
+ self.addAction(a)
+ self.addSeparator()
+
+ a = QAction(self.tr("Options:"), self)
+ a.setEnabled(False)
+ self.addAction(a)
+ for optionKey in self.options:
+ a = QAction(self.options[optionKey][0], self)
+ a.setCheckable(True)
+ a.setChecked(self.options[optionKey][1])
+ a.setData(optionKey)
+ a.triggered.connect(self._updateOptions)
+ self.addAction(a)
+ self.addSeparator()
+
+ def _updateFilters(self):
+ a = self.sender()
+ self.filters[a.data()].setEnabled(a.isChecked())
+
+ def _updateOptions(self):
+ a = self.sender()
+ self.options[a.data()][1] = a.isChecked()
+
+ def columns(self, modelName):
+ if self.filters[modelName].enabled():
+ return self.filters[modelName].modelColumns()
+ else:
+ return []
+
+ def caseSensitive(self):
+ return self.options["CS"][1]
+
+ def matchWords(self):
+ return self.options["MatchWords"][1]
+
+ def regex(self):
+ return self.options["Regex"][1]
+
+ def mouseReleaseEvent(self, event):
+ # Workaround for enabling / disabling actions without closing the menu.
+ # Source: https://stackoverflow.com/a/14967212
+ action = self.activeAction()
+ if action:
+ action.setEnabled(False)
+ QMenu.mouseReleaseEvent(self, event)
+ action.setEnabled(True)
+ action.trigger()
+ else:
+ QMenu.mouseReleaseEvent(self, event)
+
+ def keyPressEvent(self, event):
+ # Workaround for enabling / disabling actions without closing the menu.
+ # Source: https://stackoverflow.com/a/14967212
+ action = self.activeAction()
+ if action and event.key() == QtCore.Qt.Key_Return:
+ action.setEnabled(False)
+ QMenu.keyPressEvent(self, event)
+ action.setEnabled(True)
+ action.trigger()
+ else:
+ QMenu.keyPressEvent(self, event)
diff --git a/manuskript/ui/search_ui.py b/manuskript/ui/search_ui.py
index d9f5c31..5052977 100644
--- a/manuskript/ui/search_ui.py
+++ b/manuskript/ui/search_ui.py
@@ -19,12 +19,12 @@ class Ui_search(object):
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setObjectName("horizontalLayout")
- self.text = QtWidgets.QLineEdit(search)
- self.text.setInputMask("")
- self.text.setFrame(False)
- self.text.setClearButtonEnabled(True)
- self.text.setObjectName("text")
- self.horizontalLayout.addWidget(self.text)
+ self.searchTextInput = QtWidgets.QLineEdit(search)
+ self.searchTextInput.setInputMask("")
+ self.searchTextInput.setFrame(False)
+ self.searchTextInput.setClearButtonEnabled(True)
+ self.searchTextInput.setObjectName("searchTextInput")
+ self.horizontalLayout.addWidget(self.searchTextInput)
self.btnOptions = QtWidgets.QPushButton(search)
self.btnOptions.setText("")
icon = QtGui.QIcon.fromTheme("edit-find")
@@ -45,5 +45,5 @@ class Ui_search(object):
def retranslateUi(self, search):
_translate = QtCore.QCoreApplication.translate
search.setWindowTitle(_translate("search", "Form"))
- self.text.setPlaceholderText(_translate("search", "Search for..."))
+ self.searchTextInput.setPlaceholderText(_translate("search", "Search for..."))
diff --git a/manuskript/ui/search_ui.ui b/manuskript/ui/search_ui.ui
index 1b63fdc..89eb0a0 100644
--- a/manuskript/ui/search_ui.ui
+++ b/manuskript/ui/search_ui.ui
@@ -35,7 +35,7 @@
0
-
-
+
diff --git a/manuskript/ui/views/corkDelegate.py b/manuskript/ui/views/corkDelegate.py
index 70ff19e..bd776c1 100644
--- a/manuskript/ui/views/corkDelegate.py
+++ b/manuskript/ui/views/corkDelegate.py
@@ -43,11 +43,19 @@ class corkDelegate(QStyledItemDelegate):
return QStyledItemDelegate.editorEvent(self, event, model, option, index)
def createEditor(self, parent, option, index):
+ # When the user performs a global search and selects an Outline result (title or summary), the
+ # associated chapter is selected in cork view, triggering a call to this method with the results
+ # list widget set in self.sender(). In this case we store the searched column so we know which
+ # editor should be created.
+ searchedColumn = None
+ if self.sender() is not None and self.sender().objectName() == 'result' and self.sender().currentItem():
+ searchedColumn = self.sender().currentItem().data(Qt.UserRole).column()
+
self.updateRects(option, index)
bgColor = self.bgColors.get(index, "white")
- if self.mainLineRect.contains(self.lastPos):
+ if searchedColumn == Outline.summarySentence or (self.lastPos is not None and self.mainLineRect.contains(self.lastPos)):
# One line summary
self.editing = Outline.summarySentence
edt = QLineEdit(parent)
@@ -64,7 +72,7 @@ class corkDelegate(QStyledItemDelegate):
edt.setStyleSheet("background: {}; color: black;".format(bgColor))
return edt
- elif self.titleRect.contains(self.lastPos):
+ elif searchedColumn == Outline.title or (self.lastPos is not None and self.titleRect.contains(self.lastPos)):
# Title
self.editing = Outline.title
edt = QLineEdit(parent)