6
6
__docformat__ = 'restructuredtext en'
9
from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, QTimer, \
10
QApplication, QPoint, QItemDelegate, QStyleOptionViewItem, \
11
QStyle, QEvent, pyqtSignal
9
from PyQt4.Qt import QLineEdit, QAbstractListModel, Qt, \
10
QApplication, QCompleter
13
12
from calibre.utils.icu import sort_key, lower
14
13
from calibre.gui2 import NONE
15
from calibre.gui2.widgets import EnComboBox
17
class CompleterItemDelegate(QItemDelegate): # {{{
19
''' Renders the current item as thought it were selected '''
21
def __init__(self, view):
23
QItemDelegate.__init__(self, view)
25
def paint(self, p, opt, idx):
26
opt = QStyleOptionViewItem(opt)
27
opt.showDecorationSelected = True
28
if self.view.currentIndex() == idx:
29
opt.state |= QStyle.State_HasFocus
30
QItemDelegate.paint(self, p, opt, idx)
34
class CompleteWindow(QListView): # {{{
37
The completion popup. For keyboard and mouse handling see
41
#: This signal is emitted when the user selects one of the listed
42
#: completions, by mouse or keyboard
43
completion_selected = pyqtSignal(object)
45
def __init__(self, widget, model):
47
QListView.__init__(self)
48
self.setVisible(False)
49
self.setParent(None, Qt.Popup)
50
self.setAlternatingRowColors(True)
51
self.setFocusPolicy(Qt.NoFocus)
52
self._d = CompleterItemDelegate(self)
53
self.setItemDelegate(self._d)
55
self.setFocusProxy(widget)
56
self.installEventFilter(self)
57
self.clicked.connect(self.do_selected)
58
self.entered.connect(self.do_entered)
59
self.setMouseTracking(True)
61
def do_entered(self, idx):
63
self.setCurrentIndex(idx)
65
def do_selected(self, idx=None):
66
idx = self.currentIndex() if idx is None else idx
68
data = unicode(self.model().data(idx, Qt.DisplayRole))
69
self.completion_selected.emit(data)
72
def eventFilter(self, o, e):
75
if e.type() == e.KeyPress:
77
if key in (Qt.Key_Escape, Qt.Key_Backtab) or \
78
(key == Qt.Key_F4 and (e.modifiers() & Qt.AltModifier)):
81
elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
82
if key == Qt.Key_Tab and not self.currentIndex().isValid():
83
if self.model().rowCount() > 0:
84
self.setCurrentIndex(self.model().index(0))
87
elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp,
90
# Send key event to associated line edit
91
self.widget.eat_focus_out = False
95
self.widget.eat_focus_out = True
96
if not self.widget.hasFocus():
97
# Line edit lost focus
100
# Line edit consumed event
102
elif e.type() == e.MouseButtonPress:
103
# Hide popup if user clicks outside it, otherwise
104
# pass event to popup
105
if not self.underMouse():
108
elif e.type() in (e.InputMethod, e.ShortcutOverride):
109
QApplication.sendEvent(self.widget, e)
111
return False # Do not filter this event
14
from calibre.gui2.widgets import EnComboBox, LineEditECM
115
16
class CompleteModel(QAbstractListModel):
117
18
def __init__(self, parent=None):
118
19
QAbstractListModel.__init__(self, parent)
120
self.space_before_sep = False
122
self.lowered_items = []
125
22
def set_items(self, items):
126
23
items = [unicode(x.strip()) for x in items]
127
24
self.items = list(sorted(items, key=lambda x: sort_key(x)))
128
25
self.lowered_items = [lower(x) for x in self.items]
132
28
def rowCount(self, *args):
133
return len(self.matches)
29
return len(self.items)
135
31
def data(self, index, role):
136
32
if role == Qt.DisplayRole:
139
return self.matches[r]
140
36
except IndexError:
144
def get_matches(self, prefix):
146
Return all matches that (case insensitively) start with prefix
148
prefix = lower(prefix)
151
for i, test in enumerate(self.lowered_items):
152
if test.startswith(prefix):
153
ans.append(self.items[i])
156
def update_matches(self, matches):
157
self.matches = matches
160
class MultiCompleteLineEdit(QLineEdit):
41
class MultiCompleteLineEdit(QLineEdit, LineEditECM):
162
43
A line edit that completes on multiple items separated by a
163
44
separator. Use the :meth:`update_items_cache` to set the list of
171
52
def __init__(self, parent=None):
172
self.eat_focus_out = True
173
self.max_visible_items = 7
174
self.current_prefix = None
175
53
QLineEdit.__init__(self, parent)
56
self.space_before_sep = False
57
self.add_separator = True
58
self.original_cursor_pos = None
177
60
self._model = CompleteModel(parent=self)
178
self.complete_window = CompleteWindow(self, self._model)
61
self._completer = c = QCompleter(self._model, self)
63
c.setCompletionMode(QCompleter.PopupCompletion)
64
c.setCaseSensitivity(Qt.CaseInsensitive)
65
c.setModelSorting(QCompleter.UnsortedModel)
66
c.setCompletionRole(Qt.DisplayRole)
68
p.setMouseTracking(True)
69
p.entered.connect(self.item_entered)
70
c.popup().setAlternatingRowColors(True)
72
c.activated.connect(self.completion_selected,
73
type=Qt.QueuedConnection)
179
74
self.textEdited.connect(self.text_edited)
180
self.complete_window.completion_selected.connect(self.completion_selected)
181
self.installEventFilter(self)
184
77
def update_items_cache(self, complete_items):
190
83
def set_space_before_sep(self, space_before):
191
84
self.space_before_sep = space_before
86
def set_add_separator(self, what):
87
self.add_separator = bool(what)
195
def eventFilter(self, o, e):
196
if self.eat_focus_out and o is self and e.type() == QEvent.FocusOut:
197
if self.complete_window.isVisible():
198
return True # Filter this event since the cw is visible
199
return QLineEdit.eventFilter(self, o, e)
201
def hide_completion_window(self):
202
self.complete_window.hide()
91
def item_entered(self, idx):
92
self._completer.popup().setCurrentIndex(idx)
205
94
def text_edited(self, *args):
206
95
self.update_completions()
96
self._completer.complete()
208
98
def update_completions(self):
209
99
' Update the list of completions '
210
if not self.complete_window.isVisible() and not self.hasFocus():
212
cpos = self.cursorPosition()
100
self.original_cursor_pos = cpos = self.cursorPosition()
213
101
text = unicode(self.text())
214
102
prefix = text[:cpos]
215
103
self.current_prefix = prefix
216
104
complete_prefix = prefix.lstrip()
218
complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip()
220
matches = self._model.get_matches(complete_prefix)
221
self.update_complete_window(matches)
106
complete_prefix = prefix.split(self.sep)[-1].lstrip()
107
self._completer.setCompletionPrefix(complete_prefix)
223
109
def get_completed_text(self, text):
225
Get completed text from current cursor position and the completion
110
'Get completed text in before and after parts'
228
111
if self.sep is None:
231
cursor_pos = self.cursorPosition()
232
before_text = unicode(self.text())[:cursor_pos]
233
after_text = unicode(self.text())[cursor_pos:]
234
after_parts = after_text.split(self.sep)
235
if len(after_parts) < 3 and not after_parts[-1].strip():
237
prefix_len = len(before_text.split(self.sep)[-1].lstrip())
239
before_text[:cursor_pos - prefix_len] + text + after_text
114
cursor_pos = self.original_cursor_pos
115
if cursor_pos is None:
116
cursor_pos = self.cursorPosition()
117
self.original_cursor_pos = None
119
curtext = unicode(self.text())
120
before_text = curtext[:cursor_pos]
121
after_text = curtext[cursor_pos:].rstrip()
122
# Remove the completion prefix from the before text
123
before_text = self.sep.join(before_text.split(self.sep)[:-1]).rstrip()
125
# Add the separator to the end of before_text
126
if self.space_before_sep:
128
before_text += self.sep + ' '
129
if self.add_separator or after_text:
130
# Add separator to the end of completed text
131
if self.space_before_sep:
132
text = text.rstrip() + ' '
133
completed_text = text + self.sep + ' '
135
completed_text = text
136
return before_text + completed_text, after_text
241
138
def completion_selected(self, text):
242
prefix_len, ctext = self.get_completed_text(text)
245
self.setCursorPosition(len(ctext))
247
cursor_pos = self.cursorPosition()
249
self.setCursorPosition(cursor_pos - prefix_len + len(text))
251
def update_complete_window(self, matches):
252
self._model.update_matches(matches)
254
self.show_complete_window()
256
self.complete_window.hide()
259
def position_complete_window(self):
260
popup = self.complete_window
261
screen = QApplication.desktop().availableGeometry(self)
262
h = (popup.sizeHintForRow(0) * min(self.max_visible_items,
263
popup.model().rowCount()) + 3) + 3
264
hsb = popup.horizontalScrollBar()
265
if hsb and hsb.isVisible():
266
h += hsb.sizeHint().height()
269
pos = self.mapToGlobal(QPoint(0, self.height() - 2))
272
if w > screen.width():
274
if (pos.x() + w) > (screen.x() + screen.width()):
275
pos.setX(screen.x() + screen.width() - w)
276
if (pos.x() < screen.x()):
279
top = pos.y() - rh - screen.top() + 2
280
bottom = screen.bottom() - pos.y()
281
h = max(h, popup.minimumHeight())
283
h = min(max(top, bottom), h)
285
pos.setY(pos.y() - h - rh + 2)
287
popup.setGeometry(pos.x(), pos.y(), w, h)
290
def show_complete_window(self):
291
self.position_complete_window()
292
self.complete_window.show()
294
def moveEvent(self, ev):
295
ret = QLineEdit.moveEvent(self, ev)
296
QTimer.singleShot(0, self.position_complete_window)
299
def resizeEvent(self, ev):
300
ret = QLineEdit.resizeEvent(self, ev)
301
QTimer.singleShot(0, self.position_complete_window)
139
before_text, after_text = self.get_completed_text(unicode(text))
140
self.setText(before_text + after_text)
141
self.setCursorPosition(len(before_text))
305
143
@dynamic_property
306
144
def all_items(self):