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 | from IPython.utils.traitlets import Bool, Enum, Int |
|
22 | from IPython.utils.traitlets import Bool, Enum, Int | |
23 | from ansi_code_processor import QtAnsiCodeProcessor |
|
23 | from ansi_code_processor import QtAnsiCodeProcessor | |
24 | from completion_widget import CompletionWidget |
|
24 | from completion_widget import CompletionWidget | |
|
25 | from kill_ring import QtKillRing | |||
25 |
|
26 | |||
26 | #----------------------------------------------------------------------------- |
|
27 | #----------------------------------------------------------------------------- | |
27 | # Functions |
|
28 | # Functions | |
@@ -173,6 +174,7 class ConsoleWidget(Configurable, QtGui.QWidget): | |||||
173 | self._filter_drag = False |
|
174 | self._filter_drag = False | |
174 | self._filter_resize = False |
|
175 | self._filter_resize = False | |
175 | self._html_exporter = HtmlExporter(self._control) |
|
176 | self._html_exporter = HtmlExporter(self._control) | |
|
177 | self._kill_ring = QtKillRing(self._control) | |||
176 | self._prompt = '' |
|
178 | self._prompt = '' | |
177 | self._prompt_html = None |
|
179 | self._prompt_html = None | |
178 | self._prompt_pos = 0 |
|
180 | self._prompt_pos = 0 | |
@@ -953,7 +955,7 class ConsoleWidget(Configurable, QtGui.QWidget): | |||||
953 | cursor.movePosition(QtGui.QTextCursor.Right, |
|
955 | cursor.movePosition(QtGui.QTextCursor.Right, | |
954 | QtGui.QTextCursor.KeepAnchor, |
|
956 | QtGui.QTextCursor.KeepAnchor, | |
955 | len(self._continuation_prompt)) |
|
957 | len(self._continuation_prompt)) | |
956 | cursor.removeSelectedText() |
|
958 | self._kill_ring.kill_cursor(cursor) | |
957 | intercepted = True |
|
959 | intercepted = True | |
958 |
|
960 | |||
959 | elif key == QtCore.Qt.Key_L: |
|
961 | elif key == QtCore.Qt.Key_L: | |
@@ -976,11 +978,12 class ConsoleWidget(Configurable, QtGui.QWidget): | |||||
976 | QtGui.QTextCursor.KeepAnchor) |
|
978 | QtGui.QTextCursor.KeepAnchor) | |
977 | cursor.movePosition(QtGui.QTextCursor.Right, |
|
979 | cursor.movePosition(QtGui.QTextCursor.Right, | |
978 | QtGui.QTextCursor.KeepAnchor, offset) |
|
980 | QtGui.QTextCursor.KeepAnchor, offset) | |
979 | cursor.removeSelectedText() |
|
981 | self._kill_ring.kill_cursor(cursor) | |
980 | intercepted = True |
|
982 | intercepted = True | |
981 |
|
983 | |||
982 | elif key == QtCore.Qt.Key_Y: |
|
984 | elif key == QtCore.Qt.Key_Y: | |
983 |
self. |
|
985 | self._keep_cursor_in_buffer() | |
|
986 | self._kill_ring.yank() | |||
984 | intercepted = True |
|
987 | intercepted = True | |
985 |
|
988 | |||
986 | elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete): |
|
989 | elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete): | |
@@ -1005,16 +1008,20 class ConsoleWidget(Configurable, QtGui.QWidget): | |||||
1005 | self._set_cursor(self._get_word_end_cursor(position)) |
|
1008 | self._set_cursor(self._get_word_end_cursor(position)) | |
1006 | intercepted = True |
|
1009 | intercepted = True | |
1007 |
|
1010 | |||
|
1011 | elif key == QtCore.Qt.Key_Y: | |||
|
1012 | self._kill_ring.rotate() | |||
|
1013 | intercepted = True | |||
|
1014 | ||||
1008 | elif key == QtCore.Qt.Key_Backspace: |
|
1015 | elif key == QtCore.Qt.Key_Backspace: | |
1009 | cursor = self._get_word_start_cursor(position) |
|
1016 | cursor = self._get_word_start_cursor(position) | |
1010 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) |
|
1017 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) | |
1011 | cursor.removeSelectedText() |
|
1018 | self._kill_ring.kill_cursor(cursor) | |
1012 | intercepted = True |
|
1019 | intercepted = True | |
1013 |
|
1020 | |||
1014 | elif key == QtCore.Qt.Key_D: |
|
1021 | elif key == QtCore.Qt.Key_D: | |
1015 | cursor = self._get_word_end_cursor(position) |
|
1022 | cursor = self._get_word_end_cursor(position) | |
1016 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) |
|
1023 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) | |
1017 | cursor.removeSelectedText() |
|
1024 | self._kill_ring.kill_cursor(cursor) | |
1018 | intercepted = True |
|
1025 | intercepted = True | |
1019 |
|
1026 | |||
1020 | elif key == QtCore.Qt.Key_Delete: |
|
1027 | elif key == QtCore.Qt.Key_Delete: |
General Comments 0
You need to be logged in to leave comments.
Login now