Skip to content

Commit 7624935

Browse files
author
Michał Góral
committedMay 27, 2015
Added search bar to the subtitle view.
- it is accessible via menu or ctrl-f key shortcut - search is "smart case sensitive": it is case sensitive only when there's at least one upper case letter in searched phrase (@see vim's 'set smartcase') - one search bar per subtitle view - supports circular iterating over the search results - when user clicks somewhere on the subtitle list, then showing the next (or previous) element will start from that selection (not from the last position remembered by SearchIter)
1 parent dea4892 commit 7624935

File tree

5 files changed

+204
-10
lines changed

5 files changed

+204
-10
lines changed
 

‎subconvert/gui/Detail.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from subconvert.utils.Locale import _, P_
2626

2727
from PyQt4.QtGui import QListWidget, QTreeWidget, QComboBox, QAction, QIcon, QMessageBox
28-
from PyQt4.QtGui import QAbstractSpinBox, QStyledItemDelegate, QValidator, QBrush
28+
from PyQt4.QtGui import QAbstractSpinBox, QStyledItemDelegate, QValidator, QBrush, QLineEdit
2929
from PyQt4.QtCore import Qt, pyqtSignal
3030

3131
# define globally to avoid mistakes
@@ -287,3 +287,13 @@ def exec(self):
287287
"Errors occured when trying to open following files:",
288288
fileListSize))
289289
super().exec()
290+
291+
class SearchEdit(QLineEdit):
292+
escapePressed = pyqtSignal()
293+
def __init__(self, parent = None):
294+
super().__init__(parent)
295+
296+
def keyPressEvent(self, ev):
297+
if ev.key() == Qt.Key_Escape:
298+
self.escapePressed.emit()
299+
super().keyPressEvent(ev)

‎subconvert/gui/MainWindow.py

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import os
2323
import logging
24+
import bisect
2425
from string import Template
2526

2627
from PyQt4.QtGui import QMainWindow, QWidget, QFileDialog, QVBoxLayout, QAction, QIcon, qApp
@@ -201,6 +202,10 @@ def __initActions(self):
201202
"list-remove", _("&Remove subtitles"), None, "delete",
202203
connection = lambda: self._tabs.currentPage().removeSelectedSubtitles())
203204

205+
self._actions["findSub"] = af.create(
206+
"edit-find", _("&Find..."), None, "ctrl+f",
207+
connection = lambda: self._tabs.currentPage().highlight())
208+
204209
# Video
205210
self._videoRatios = [(4, 3), (14, 9), (14, 10), (16, 9), (16, 10)]
206211
self._actions["openVideo"] = af.create(
@@ -261,6 +266,7 @@ def __initMenuBar(self):
261266
subtitlesMenu.addAction(self._actions["insertSub"])
262267
subtitlesMenu.addAction(self._actions["addSub"])
263268
subtitlesMenu.addAction(self._actions["removeSub"])
269+
subtitlesMenu.addAction(self._actions["findSub"])
264270
subtitlesMenu.addSeparator()
265271
self._fpsMenu = subtitlesMenu.addMenu(_("&Frames per second"))
266272
self._fpsMenu.addSeparator()
@@ -411,6 +417,7 @@ def __updateMenuItemsState(self):
411417
self._actions["insertSub"].setEnabled(not tabIsStatic)
412418
self._actions["addSub"].setEnabled(not tabIsStatic)
413419
self._actions["removeSub"].setEnabled(not tabIsStatic)
420+
self._actions["findSub"].setEnabled(not tabIsStatic)
414421

415422
self._actions["videoJump"].setEnabled(not tabIsStatic)
416423

‎subconvert/gui/SubtitleTabs.py

+114-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#-*- coding: utf-8 -*-
22

33
"""
4-
Copyright (C) 2011, 2012, 2013 Michal Goral.
4+
Copyright (C) 2011-2015 Michal Goral.
55
66
This file is part of Subconvert
77
@@ -22,11 +22,13 @@
2222
import os
2323
import logging
2424
import encodings
25-
from collections import OrderedDict
25+
import bisect
26+
from enum import Enum
2627

2728
from PyQt4.QtGui import QWidget, QHBoxLayout, QVBoxLayout, QGridLayout, QIcon, QTreeWidgetItem
2829
from PyQt4.QtGui import QTableView, QHeaderView, QStandardItemModel, QStandardItem, QSizePolicy
2930
from PyQt4.QtGui import QMessageBox, QAbstractItemView, QAction, QMenu, QCursor, QFileDialog
31+
from PyQt4.QtGui import QPushButton
3032
from PyQt4.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer
3133

3234
from subconvert.parsing.FrameTime import FrameTime
@@ -36,9 +38,11 @@
3638
from subconvert.utils.SubSettings import SubSettings
3739
from subconvert.utils.PropertyFile import SubtitleProperties, PropertiesFileApplier
3840
from subconvert.utils.SubFile import File
41+
from subconvert.utils.SubtitleSearch import SearchIterator, matchText
3942
from subconvert.gui.FileDialogs import FileDialog
4043
from subconvert.gui.Detail import ActionFactory, SubtitleList, ComboBoxWithHistory, FPS_VALUES
4144
from subconvert.gui.Detail import CustomDataRoles, SubListItemDelegate, DisableSignalling
45+
from subconvert.gui.Detail import SearchEdit
4246
from subconvert.gui.SubtitleCommands import *
4347

4448
log = logging.getLogger('Subconvert.%s' % __name__)
@@ -495,6 +499,9 @@ def __initWidgets(self):
495499
self._subList.setItemDelegateForColumn(1, subListDelegate)
496500
self._subList.horizontalHeader().setResizeMode(2, QHeaderView.Stretch)
497501

502+
self._searchBar = SearchBar(self)
503+
self._searchBar.hide()
504+
498505
# Top toolbar
499506
toolbar = QHBoxLayout()
500507
toolbar.setAlignment(Qt.AlignLeft)
@@ -507,6 +514,7 @@ def __initWidgets(self):
507514
grid.setContentsMargins(0, 3, 0, 0)
508515
grid.addLayout(toolbar, 0, 0, 1, 1) # stretch to the right
509516
grid.addWidget(self._subList, 1, 0)
517+
grid.addWidget(self._searchBar, 2, 0)
510518
self.setLayout(grid)
511519

512520
def __initContextMenu(self):
@@ -659,6 +667,10 @@ def removeSelectedSubtitles(self):
659667
else:
660668
self._subList.selectRow(self._model.rowCount() - 1)
661669

670+
def highlight(self):
671+
self._searchBar.show()
672+
self._searchBar.highlight()
673+
662674
def showContextMenu(self):
663675
self._contextMenu.exec(QCursor.pos())
664676

@@ -743,14 +755,19 @@ def updateTab(self):
743755
self.refreshSubtitles()
744756

745757
def selectedSubtitles(self):
758+
rows = self.selectedRows()
759+
subtitleList = [self.subtitles[row] for row in rows]
760+
return subtitleList
761+
762+
def selectedRows(self):
746763
indices = self._subList.selectedIndexes()
747-
if len(indices) > 0:
748-
tempDict = OrderedDict.fromkeys([index.row() for index in indices])
749-
rows = list(tempDict)
750-
rows.sort()
751-
subtitleList = [self.subtitles[row] for row in rows]
752-
return subtitleList
753-
return []
764+
# unique list
765+
rows = list(set([index.row() for index in indices]))
766+
rows.sort()
767+
return rows
768+
769+
def selectRow(self, row):
770+
self._subList.selectRow(row)
754771

755772
@property
756773
def filePath(self):
@@ -783,3 +800,91 @@ def outputEncoding(self):
783800
@property
784801
def outputFormat(self):
785802
return self.data.outputFormat
803+
804+
class SearchBar(QWidget):
805+
class SearchDirection(Enum):
806+
Forward = 1
807+
Backward = 2
808+
809+
def __init__(self, parent):
810+
super(SearchBar, self).__init__(parent)
811+
812+
self._sit = None
813+
814+
self._editor = SearchEdit(self)
815+
self._prevButton = QPushButton("<", self)
816+
self._nextButton = QPushButton(">", self)
817+
self._closeButton = QPushButton(QIcon.fromTheme("window-close"), "", self)
818+
819+
layout = QHBoxLayout()
820+
layout.setAlignment(Qt.AlignLeft)
821+
layout.setSpacing(0)
822+
823+
layout.addWidget(self._editor)
824+
layout.addWidget(self._prevButton)
825+
layout.addWidget(self._nextButton)
826+
layout.addWidget(self._closeButton)
827+
layout.setContentsMargins(1, 1, 1, 1)
828+
829+
self.setLayout(layout)
830+
831+
self._nextButton.clicked.connect(self.next)
832+
self._prevButton.clicked.connect(self.prev)
833+
self._closeButton.clicked.connect(self.hide)
834+
self._editor.textChanged.connect(self._reset)
835+
self._editor.returnPressed.connect(self.next)
836+
self._editor.escapePressed.connect(self.hide)
837+
838+
def highlight(self):
839+
self._editor.setFocus()
840+
self._editor.selectAll()
841+
842+
def next(self):
843+
self._search(self.SearchDirection.Forward)
844+
845+
def prev(self):
846+
self._search(self.SearchDirection.Backward)
847+
848+
def _search(self, direction):
849+
if self._editor.text() == "":
850+
self._reset()
851+
return
852+
853+
if self._sit is None:
854+
data = self.parent().data
855+
case = any(map(str.isupper, self._editor.text()))
856+
self._sit = SearchIterator(data.subtitles,
857+
lambda sub, text = self._editor.text(), case = case: matchText(sub, text, case))
858+
859+
self._updateIteratorPositionFromSelection(direction)
860+
861+
fn = self._sit.next if direction == self.SearchDirection.Forward else self._sit.prev
862+
try:
863+
subNo = fn()
864+
self.parent().selectRow(subNo)
865+
except StopIteration:
866+
self._searchError()
867+
868+
def _updateIteratorPositionFromSelection(self, direction):
869+
selections = self.parent().selectedRows()
870+
startRow = selections[-1] if len(selections) > 0 else -1
871+
872+
if startRow == -1 or len(self._sit.range()) == 0:
873+
return
874+
875+
# When iterator points at different row, it means that user changed it
876+
if startRow != self._sit.get():
877+
pos = None
878+
if direction == self.SearchDirection.Forward:
879+
pos = bisect.bisect_right(self._sit.range(), startRow) - 1
880+
else:
881+
pos = bisect.bisect_left(self._sit.range(), startRow)
882+
self._sit.setpos(pos)
883+
884+
def _searchError(self):
885+
self._editor.setStyleSheet("background-color: #CD5555")
886+
887+
def _reset(self):
888+
self._sit = None
889+
self._editor.setStyleSheet("")
890+

‎subconvert/utils/Makefile.am

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ utils_PYTHON = \
1010
SubFile.py \
1111
SubSettings.py \
1212
SubtitleData.py \
13+
SubtitleSearch.py \
1314
version.py \
1415
VideoPlayer.py
1516

‎subconvert/utils/SubtitleSearch.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#-*- coding: utf-8 -*-
2+
3+
"""
4+
Copyright (C) 2011-2015 Michal Goral.
5+
6+
This file is part of Subconvert
7+
8+
Subconvert is free software: you can redistribute it and/or modify
9+
it under the terms of the GNU General Public License as published by
10+
the Free Software Foundation, either version 3 of the License, or
11+
(at your option) any later version.
12+
13+
Subconvert is distributed in the hope that it will be useful,
14+
but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
GNU General Public License for more details.
17+
18+
You should have received a copy of the GNU General Public License
19+
along with Subconvert. If not, see <http://www.gnu.org/licenses/>.
20+
"""
21+
22+
def matchText(sub, text, case):
23+
if (text == ""): return False
24+
if case is True:
25+
if text in sub.text: return True
26+
else:
27+
if text.lower() in sub.text.lower(): return True
28+
return False
29+
30+
class SearchIterator:
31+
def __init__(self, subs, matcher):
32+
"""
33+
subs: list of subtitles (list(subconvert.parsing.Subtitle))
34+
matcher: function returning True when single subtitle matches a given criteria
35+
"""
36+
self._range = [i for i, sub in enumerate(subs) if matcher(sub)]
37+
self._idx = -1
38+
39+
def __iter__(self):
40+
return self
41+
42+
def range(self):
43+
return self._range
44+
45+
def get(self):
46+
if len(self._range) == 0:
47+
raise IndexError("range is empty")
48+
return self._get(self._idx)
49+
50+
def next(self):
51+
return self._get(self._idx + 1)
52+
53+
def prev(self):
54+
return self._get(self._idx - 1)
55+
56+
def setpos(self, pos):
57+
self._idx = pos
58+
59+
def _indexInRange(self, index):
60+
if index < 0:
61+
index = len(self._range) - 1
62+
elif index >= len(self._range):
63+
index = 0
64+
return index
65+
66+
def _get(self, index):
67+
if len(self._range) == 0:
68+
raise StopIteration
69+
70+
self._idx = self._indexInRange(index)
71+
return self._range[self._idx]

0 commit comments

Comments
 (0)
Please sign in to comment.