~stub/ubuntu/precise/calibre/devel

« back to all changes in this revision

Viewing changes to src/calibre/gui2/complete.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2011-04-12 11:29:25 UTC
  • mfrom: (42.1.2 sid)
  • Revision ID: james.westby@ubuntu.com-20110412112925-c7171kt2bb5rmft4
Tags: 0.7.50+dfsg-2
* debian/control: Build with libpodofo-dev to enable PDF metadata.
  (Closes: #619632)
* debian/control: Add libboost1.42-dev build dependency. Apparently it is
  needed in some setups. (Closes: #619807)
* debian/rules: Call dh_sip to generate a proper sip API dependency, to
  prevent crashes like #616372 for partial upgrades.
* debian/control: Bump python-qt4 dependency to >= 4.8.3-2, which reportedly
  fixes crashes on startup. (Closes: #619701, #620125)

Show diffs side-by-side

added added

removed removed

Lines of Context:
6
6
__docformat__ = 'restructuredtext en'
7
7
 
8
8
 
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
12
11
 
13
12
from calibre.utils.icu import sort_key, lower
14
13
from calibre.gui2 import NONE
15
 
from calibre.gui2.widgets import EnComboBox
16
 
 
17
 
class CompleterItemDelegate(QItemDelegate): # {{{
18
 
 
19
 
    ''' Renders the current item as thought it were selected '''
20
 
 
21
 
    def __init__(self, view):
22
 
        self.view = view
23
 
        QItemDelegate.__init__(self, view)
24
 
 
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)
31
 
 
32
 
# }}}
33
 
 
34
 
class CompleteWindow(QListView): # {{{
35
 
 
36
 
    '''
37
 
    The completion popup. For keyboard and mouse handling see
38
 
    :meth:`eventFilter`.
39
 
    '''
40
 
 
41
 
    #: This signal is emitted when the user selects one of the listed
42
 
    #: completions, by mouse or keyboard
43
 
    completion_selected = pyqtSignal(object)
44
 
 
45
 
    def __init__(self, widget, model):
46
 
        self.widget = widget
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)
54
 
        self.setModel(model)
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)
60
 
 
61
 
    def do_entered(self, idx):
62
 
        if idx.isValid():
63
 
            self.setCurrentIndex(idx)
64
 
 
65
 
    def do_selected(self, idx=None):
66
 
        idx = self.currentIndex() if idx is None else idx
67
 
        if idx.isValid():
68
 
            data = unicode(self.model().data(idx, Qt.DisplayRole))
69
 
            self.completion_selected.emit(data)
70
 
        self.hide()
71
 
 
72
 
    def eventFilter(self, o, e):
73
 
        if o is not self:
74
 
            return False
75
 
        if e.type() == e.KeyPress:
76
 
            key = e.key()
77
 
            if key in (Qt.Key_Escape, Qt.Key_Backtab) or \
78
 
                    (key == Qt.Key_F4 and (e.modifiers() & Qt.AltModifier)):
79
 
                self.hide()
80
 
                return True
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))
85
 
                self.do_selected()
86
 
                return True
87
 
            elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp,
88
 
                    Qt.Key_PageDown):
89
 
                return False
90
 
            # Send key event to associated line edit
91
 
            self.widget.eat_focus_out = False
92
 
            try:
93
 
                self.widget.event(e)
94
 
            finally:
95
 
                self.widget.eat_focus_out = True
96
 
            if not self.widget.hasFocus():
97
 
                # Line edit lost focus
98
 
                self.hide()
99
 
            if e.isAccepted():
100
 
                # Line edit consumed event
101
 
                return True
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():
106
 
                self.hide()
107
 
                return True
108
 
        elif e.type() in (e.InputMethod, e.ShortcutOverride):
109
 
            QApplication.sendEvent(self.widget, e)
110
 
 
111
 
        return False # Do not filter this event
112
 
 
113
 
# }}}
 
14
from calibre.gui2.widgets import EnComboBox, LineEditECM
114
15
 
115
16
class CompleteModel(QAbstractListModel):
116
17
 
117
18
    def __init__(self, parent=None):
118
19
        QAbstractListModel.__init__(self, parent)
119
 
        self.sep = ','
120
 
        self.space_before_sep = False
121
20
        self.items = []
122
 
        self.lowered_items = []
123
 
        self.matches = []
124
21
 
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]
129
 
        self.matches = []
130
26
        self.reset()
131
27
 
132
28
    def rowCount(self, *args):
133
 
        return len(self.matches)
 
29
        return len(self.items)
134
30
 
135
31
    def data(self, index, role):
136
32
        if role == Qt.DisplayRole:
137
33
            r = index.row()
138
34
            try:
139
 
                return self.matches[r]
 
35
                return self.items[r]
140
36
            except IndexError:
141
37
                pass
142
38
        return NONE
143
39
 
144
 
    def get_matches(self, prefix):
145
 
        '''
146
 
        Return all matches that (case insensitively) start with prefix
147
 
        '''
148
 
        prefix = lower(prefix)
149
 
        ans = []
150
 
        if prefix:
151
 
            for i, test in enumerate(self.lowered_items):
152
 
                if test.startswith(prefix):
153
 
                    ans.append(self.items[i])
154
 
        return ans
155
 
 
156
 
    def update_matches(self, matches):
157
 
        self.matches = matches
158
 
        self.reset()
159
 
 
160
 
class MultiCompleteLineEdit(QLineEdit):
 
40
 
 
41
class MultiCompleteLineEdit(QLineEdit, LineEditECM):
161
42
    '''
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
169
50
    '''
170
51
 
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)
176
54
 
 
55
        self.sep = ','
 
56
        self.space_before_sep = False
 
57
        self.add_separator = True
 
58
        self.original_cursor_pos = None
 
59
 
177
60
        self._model = CompleteModel(parent=self)
178
 
        self.complete_window = CompleteWindow(self, self._model)
 
61
        self._completer = c = QCompleter(self._model, self)
 
62
        c.setWidget(self)
 
63
        c.setCompletionMode(QCompleter.PopupCompletion)
 
64
        c.setCaseSensitivity(Qt.CaseInsensitive)
 
65
        c.setModelSorting(QCompleter.UnsortedModel)
 
66
        c.setCompletionRole(Qt.DisplayRole)
 
67
        p = c.popup()
 
68
        p.setMouseTracking(True)
 
69
        p.entered.connect(self.item_entered)
 
70
        c.popup().setAlternatingRowColors(True)
 
71
 
 
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)
182
75
 
183
76
    # Interface {{{
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
192
85
 
 
86
    def set_add_separator(self, what):
 
87
        self.add_separator = bool(what)
 
88
 
193
89
    # }}}
194
90
 
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)
200
 
 
201
 
    def hide_completion_window(self):
202
 
        self.complete_window.hide()
203
 
 
 
91
    def item_entered(self, idx):
 
92
        self._completer.popup().setCurrentIndex(idx)
204
93
 
205
94
    def text_edited(self, *args):
206
95
        self.update_completions()
 
96
        self._completer.complete()
207
97
 
208
98
    def update_completions(self):
209
99
        ' Update the list of completions '
210
 
        if not self.complete_window.isVisible() and not self.hasFocus():
211
 
            return
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()
217
105
        if self.sep:
218
 
            complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip()
219
 
 
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)
222
108
 
223
109
    def get_completed_text(self, text):
224
 
        '''
225
 
        Get completed text from current cursor position and the completion
226
 
        text
227
 
        '''
 
110
        'Get completed text in before and after parts'
228
111
        if self.sep is None:
229
 
            return -1, text
 
112
            return text, ''
230
113
        else:
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():
236
 
                after_text = u''
237
 
            prefix_len = len(before_text.split(self.sep)[-1].lstrip())
238
 
            return prefix_len, \
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
 
118
            # Split text
 
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()
 
124
            if before_text:
 
125
                # Add the separator to the end of before_text
 
126
                if self.space_before_sep:
 
127
                    before_text += ' '
 
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 + ' '
 
134
            else:
 
135
                completed_text = text
 
136
            return before_text + completed_text, after_text
240
137
 
241
138
    def completion_selected(self, text):
242
 
        prefix_len, ctext = self.get_completed_text(text)
243
 
        if self.sep is None:
244
 
            self.setText(ctext)
245
 
            self.setCursorPosition(len(ctext))
246
 
        else:
247
 
            cursor_pos = self.cursorPosition()
248
 
            self.setText(ctext)
249
 
            self.setCursorPosition(cursor_pos - prefix_len + len(text))
250
 
 
251
 
    def update_complete_window(self, matches):
252
 
        self._model.update_matches(matches)
253
 
        if matches:
254
 
            self.show_complete_window()
255
 
        else:
256
 
            self.complete_window.hide()
257
 
 
258
 
 
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()
267
 
 
268
 
        rh = self.height()
269
 
        pos = self.mapToGlobal(QPoint(0, self.height() - 2))
270
 
        w = self.width()
271
 
 
272
 
        if w > screen.width():
273
 
            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()):
277
 
            pos.setX(screen.x())
278
 
 
279
 
        top = pos.y() - rh - screen.top() + 2
280
 
        bottom = screen.bottom() - pos.y()
281
 
        h = max(h, popup.minimumHeight())
282
 
        if h > bottom:
283
 
            h = min(max(top, bottom), h)
284
 
            if top > bottom:
285
 
                pos.setY(pos.y() - h - rh + 2)
286
 
 
287
 
        popup.setGeometry(pos.x(), pos.y(), w, h)
288
 
 
289
 
 
290
 
    def show_complete_window(self):
291
 
        self.position_complete_window()
292
 
        self.complete_window.show()
293
 
 
294
 
    def moveEvent(self, ev):
295
 
        ret = QLineEdit.moveEvent(self, ev)
296
 
        QTimer.singleShot(0, self.position_complete_window)
297
 
        return ret
298
 
 
299
 
    def resizeEvent(self, ev):
300
 
        ret = QLineEdit.resizeEvent(self, ev)
301
 
        QTimer.singleShot(0, self.position_complete_window)
302
 
        return ret
303
 
 
 
139
        before_text, after_text = self.get_completed_text(unicode(text))
 
140
        self.setText(before_text + after_text)
 
141
        self.setCursorPosition(len(before_text))
304
142
 
305
143
    @dynamic_property
306
144
    def all_items(self):
310
148
            self._model.set_items(items)
311
149
        return property(fget=fget, fset=fset)
312
150
 
313
 
    @dynamic_property
314
 
    def sep(self):
315
 
        def fget(self):
316
 
            return self._model.sep
317
 
        def fset(self, val):
318
 
            self._model.sep = val
319
 
        return property(fget=fget, fset=fset)
320
 
 
321
 
    @dynamic_property
322
 
    def space_before_sep(self):
323
 
        def fget(self):
324
 
            return self._model.space_before_sep
325
 
        def fset(self, val):
326
 
            self._model.space_before_sep = val
327
 
        return property(fget=fget, fset=fset)
328
 
 
329
151
class MultiCompleteComboBox(EnComboBox):
330
152
 
331
153
    def __init__(self, *args):
336
158
        # item that matches case insensitively
337
159
        c = self.lineEdit().completer()
338
160
        c.setCaseSensitivity(Qt.CaseSensitive)
 
161
        self.dummy_model = CompleteModel(self)
 
162
        c.setModel(self.dummy_model)
 
163
        self.lineEdit()._completer.setWidget(self)
339
164
 
340
165
    def update_items_cache(self, complete_items):
341
166
        self.lineEdit().update_items_cache(complete_items)
346
171
    def set_space_before_sep(self, space_before):
347
172
        self.lineEdit().set_space_before_sep(space_before)
348
173
 
 
174
    def set_add_separator(self, what):
 
175
        self.lineEdit().set_add_separator(what)
 
176
 
349
177
 
350
178
 
351
179
if __name__ == '__main__':