Show More
@@ -0,0 +1,128 | |||
|
1 | """ A generic Emacs-style kill ring, as well as a Qt-specific version. | |
|
2 | """ | |
|
3 | #----------------------------------------------------------------------------- | |
|
4 | # Imports | |
|
5 | #----------------------------------------------------------------------------- | |
|
6 | ||
|
7 | # System library imports | |
|
8 | from IPython.external.qt import QtCore, QtGui | |
|
9 | ||
|
10 | #----------------------------------------------------------------------------- | |
|
11 | # Classes | |
|
12 | #----------------------------------------------------------------------------- | |
|
13 | ||
|
14 | class KillRing(object): | |
|
15 | """ A generic Emacs-style kill ring. | |
|
16 | """ | |
|
17 | ||
|
18 | def __init__(self): | |
|
19 | self.clear() | |
|
20 | ||
|
21 | def clear(self): | |
|
22 | """ Clears the kill ring. | |
|
23 | """ | |
|
24 | self._index = -1 | |
|
25 | self._ring = [] | |
|
26 | ||
|
27 | def kill(self, text): | |
|
28 | """ Adds some killed text to the ring. | |
|
29 | """ | |
|
30 | self._ring.append(text) | |
|
31 | ||
|
32 | def yank(self): | |
|
33 | """ Yank back the most recently killed text. | |
|
34 | ||
|
35 | Returns: | |
|
36 | -------- | |
|
37 | A text string or None. | |
|
38 | """ | |
|
39 | self._index = len(self._ring) | |
|
40 | return self.rotate() | |
|
41 | ||
|
42 | def rotate(self): | |
|
43 | """ Rotate the kill ring, then yank back the new top. | |
|
44 | ||
|
45 | Returns: | |
|
46 | -------- | |
|
47 | A text string or None. | |
|
48 | """ | |
|
49 | self._index -= 1 | |
|
50 | if self._index >= 0: | |
|
51 | return self._ring[self._index] | |
|
52 | return None | |
|
53 | ||
|
54 | class QtKillRing(QtCore.QObject): | |
|
55 | """ A kill ring attached to Q[Plain]TextEdit. | |
|
56 | """ | |
|
57 | ||
|
58 | #-------------------------------------------------------------------------- | |
|
59 | # QtKillRing interface | |
|
60 | #-------------------------------------------------------------------------- | |
|
61 | ||
|
62 | def __init__(self, text_edit): | |
|
63 | """ Create a kill ring attached to the specified Qt text edit. | |
|
64 | """ | |
|
65 | assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) | |
|
66 | super(QtKillRing, self).__init__() | |
|
67 | ||
|
68 | self._ring = KillRing() | |
|
69 | self._prev_yank = None | |
|
70 | self._skip_cursor = False | |
|
71 | self._text_edit = text_edit | |
|
72 | ||
|
73 | text_edit.cursorPositionChanged.connect(self._cursor_position_changed) | |
|
74 | ||
|
75 | def clear(self): | |
|
76 | """ Clears the kill ring. | |
|
77 | """ | |
|
78 | self._ring.clear() | |
|
79 | self._prev_yank = None | |
|
80 | ||
|
81 | def kill(self, text): | |
|
82 | """ Adds some killed text to the ring. | |
|
83 | """ | |
|
84 | self._ring.kill(text) | |
|
85 | ||
|
86 | def kill_cursor(self, cursor): | |
|
87 | """ Kills the text selected by the give cursor. | |
|
88 | """ | |
|
89 | text = cursor.selectedText() | |
|
90 | if text: | |
|
91 | cursor.removeSelectedText() | |
|
92 | self.kill(text) | |
|
93 | ||
|
94 | def yank(self): | |
|
95 | """ Yank back the most recently killed text. | |
|
96 | """ | |
|
97 | text = self._ring.yank() | |
|
98 | if text: | |
|
99 | self._skip_cursor = True | |
|
100 | cursor = self._text_edit.textCursor() | |
|
101 | cursor.insertText(text) | |
|
102 | self._prev_yank = text | |
|
103 | ||
|
104 | def rotate(self): | |
|
105 | """ Rotate the kill ring, then yank back the new top. | |
|
106 | """ | |
|
107 | if self._prev_yank: | |
|
108 | text = self._ring.rotate() | |
|
109 | if text: | |
|
110 | self._skip_cursor = True | |
|
111 | cursor = self._text_edit.textCursor() | |
|
112 | cursor.movePosition(QtGui.QTextCursor.Left, | |
|
113 | QtGui.QTextCursor.KeepAnchor, | |
|
114 | n = len(self._prev_yank)) | |
|
115 | cursor.insertText(text) | |
|
116 | self._prev_yank = text | |
|
117 | ||
|
118 | #-------------------------------------------------------------------------- | |
|
119 | # Protected interface | |
|
120 | #-------------------------------------------------------------------------- | |
|
121 | ||
|
122 | #------ Signal handlers ---------------------------------------------------- | |
|
123 | ||
|
124 | def _cursor_position_changed(self): | |
|
125 | if self._skip_cursor: | |
|
126 | self._skip_cursor = False | |
|
127 | else: | |
|
128 | self._prev_yank = None |
@@ -0,0 +1,83 | |||
|
1 | # Standard library imports | |
|
2 | import unittest | |
|
3 | ||
|
4 | # System library imports | |
|
5 | from IPython.external.qt import QtCore, QtGui | |
|
6 | ||
|
7 | # Local imports | |
|
8 | from IPython.frontend.qt.console.kill_ring import KillRing, QtKillRing | |
|
9 | ||
|
10 | ||
|
11 | class TestKillRing(unittest.TestCase): | |
|
12 | ||
|
13 | @classmethod | |
|
14 | def setUpClass(cls): | |
|
15 | """ Create the application for the test case. | |
|
16 | """ | |
|
17 | cls._app = QtGui.QApplication([]) | |
|
18 | cls._app.setQuitOnLastWindowClosed(False) | |
|
19 | ||
|
20 | @classmethod | |
|
21 | def tearDownClass(cls): | |
|
22 | """ Exit the application. | |
|
23 | """ | |
|
24 | QtGui.QApplication.quit() | |
|
25 | ||
|
26 | def test_generic(self): | |
|
27 | """ Does the generic kill ring work? | |
|
28 | """ | |
|
29 | ring = KillRing() | |
|
30 | self.assert_(ring.yank() is None) | |
|
31 | self.assert_(ring.rotate() is None) | |
|
32 | ||
|
33 | ring.kill('foo') | |
|
34 | self.assertEqual(ring.yank(), 'foo') | |
|
35 | self.assert_(ring.rotate() is None) | |
|
36 | self.assertEqual(ring.yank(), 'foo') | |
|
37 | ||
|
38 | ring.kill('bar') | |
|
39 | self.assertEqual(ring.yank(), 'bar') | |
|
40 | self.assertEqual(ring.rotate(), 'foo') | |
|
41 | ||
|
42 | ring.clear() | |
|
43 | self.assert_(ring.yank() is None) | |
|
44 | self.assert_(ring.rotate() is None) | |
|
45 | ||
|
46 | def test_qt_basic(self): | |
|
47 | """ Does the Qt kill ring work? | |
|
48 | """ | |
|
49 | text_edit = QtGui.QPlainTextEdit() | |
|
50 | ring = QtKillRing(text_edit) | |
|
51 | ||
|
52 | ring.kill('foo') | |
|
53 | ring.kill('bar') | |
|
54 | ring.yank() | |
|
55 | ring.rotate() | |
|
56 | ring.yank() | |
|
57 | self.assertEqual(text_edit.toPlainText(), 'foobar') | |
|
58 | ||
|
59 | text_edit.clear() | |
|
60 | ring.kill('baz') | |
|
61 | ring.yank() | |
|
62 | ring.rotate() | |
|
63 | ring.rotate() | |
|
64 | ring.rotate() | |
|
65 | self.assertEqual(text_edit.toPlainText(), 'foo') | |
|
66 | ||
|
67 | def test_qt_cursor(self): | |
|
68 | """ Does the Qt kill ring maintain state with cursor movement? | |
|
69 | """ | |
|
70 | text_edit = QtGui.QPlainTextEdit() | |
|
71 | ring = QtKillRing(text_edit) | |
|
72 | ||
|
73 | ring.kill('foo') | |
|
74 | ring.kill('bar') | |
|
75 | ring.yank() | |
|
76 | text_edit.moveCursor(QtGui.QTextCursor.Left) | |
|
77 | ring.rotate() | |
|
78 | self.assertEqual(text_edit.toPlainText(), 'bar') | |
|
79 | ||
|
80 | ||
|
81 | if __name__ == '__main__': | |
|
82 | import nose | |
|
83 | nose.main() |
@@ -22,6 +22,7 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font | |||
|
22 | 22 | from IPython.utils.traitlets import Bool, Enum, Int |
|
23 | 23 | from ansi_code_processor import QtAnsiCodeProcessor |
|
24 | 24 | from completion_widget import CompletionWidget |
|
25 | from kill_ring import QtKillRing | |
|
25 | 26 | |
|
26 | 27 | #----------------------------------------------------------------------------- |
|
27 | 28 | # Functions |
@@ -173,6 +174,7 class ConsoleWidget(Configurable, QtGui.QWidget): | |||
|
173 | 174 | self._filter_drag = False |
|
174 | 175 | self._filter_resize = False |
|
175 | 176 | self._html_exporter = HtmlExporter(self._control) |
|
177 | self._kill_ring = QtKillRing(self._control) | |
|
176 | 178 | self._prompt = '' |
|
177 | 179 | self._prompt_html = None |
|
178 | 180 | self._prompt_pos = 0 |
@@ -953,7 +955,7 class ConsoleWidget(Configurable, QtGui.QWidget): | |||
|
953 | 955 | cursor.movePosition(QtGui.QTextCursor.Right, |
|
954 | 956 | QtGui.QTextCursor.KeepAnchor, |
|
955 | 957 | len(self._continuation_prompt)) |
|
956 | cursor.removeSelectedText() | |
|
958 | self._kill_ring.kill_cursor(cursor) | |
|
957 | 959 | intercepted = True |
|
958 | 960 | |
|
959 | 961 | elif key == QtCore.Qt.Key_L: |
@@ -976,11 +978,12 class ConsoleWidget(Configurable, QtGui.QWidget): | |||
|
976 | 978 | QtGui.QTextCursor.KeepAnchor) |
|
977 | 979 | cursor.movePosition(QtGui.QTextCursor.Right, |
|
978 | 980 | QtGui.QTextCursor.KeepAnchor, offset) |
|
979 | cursor.removeSelectedText() | |
|
981 | self._kill_ring.kill_cursor(cursor) | |
|
980 | 982 | intercepted = True |
|
981 | 983 | |
|
982 | 984 | elif key == QtCore.Qt.Key_Y: |
|
983 |
self. |
|
|
985 | self._keep_cursor_in_buffer() | |
|
986 | self._kill_ring.yank() | |
|
984 | 987 | intercepted = True |
|
985 | 988 | |
|
986 | 989 | elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete): |
@@ -1005,16 +1008,20 class ConsoleWidget(Configurable, QtGui.QWidget): | |||
|
1005 | 1008 | self._set_cursor(self._get_word_end_cursor(position)) |
|
1006 | 1009 | intercepted = True |
|
1007 | 1010 | |
|
1011 | elif key == QtCore.Qt.Key_Y: | |
|
1012 | self._kill_ring.rotate() | |
|
1013 | intercepted = True | |
|
1014 | ||
|
1008 | 1015 | elif key == QtCore.Qt.Key_Backspace: |
|
1009 | 1016 | cursor = self._get_word_start_cursor(position) |
|
1010 | 1017 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) |
|
1011 | cursor.removeSelectedText() | |
|
1018 | self._kill_ring.kill_cursor(cursor) | |
|
1012 | 1019 | intercepted = True |
|
1013 | 1020 | |
|
1014 | 1021 | elif key == QtCore.Qt.Key_D: |
|
1015 | 1022 | cursor = self._get_word_end_cursor(position) |
|
1016 | 1023 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) |
|
1017 | cursor.removeSelectedText() | |
|
1024 | self._kill_ring.kill_cursor(cursor) | |
|
1018 | 1025 | intercepted = True |
|
1019 | 1026 | |
|
1020 | 1027 | elif key == QtCore.Qt.Key_Delete: |
General Comments 0
You need to be logged in to leave comments.
Login now