requirements: install newer versions of send2trash
[git-cola.git] / cola / widgets / switcher.py
blob94b3d57c9998563494892020c338812cc8ce3794
1 """Provides quick switcher"""
2 import re
4 from qtpy import QtCore
5 from qtpy import QtGui
6 from qtpy.QtCore import Qt
7 from qtpy.QtCore import Signal
9 from .. import qtutils
10 from ..widgets import defs
11 from ..widgets import standard
12 from ..widgets import text
15 def switcher_inner_view(
16 context, entries, title=None, place_holder=None, enter_action=None, parent=None
18 dialog = SwitcherInnerView(
19 context, entries, title, place_holder, enter_action, parent
21 dialog.show()
22 return dialog
25 def switcher_outer_view(context, entries, place_holder=None, parent=None):
26 dialog = SwitcherOuterView(context, entries, place_holder, parent)
27 dialog.show()
28 return dialog
31 def switcher_item(key, icon=None, name=None):
32 return SwitcherListItem(key, icon, name)
35 def moving_keys():
36 selection_move_keys = [
37 Qt.Key_Enter,
38 Qt.Key_Return,
39 Qt.Key_Up,
40 Qt.Key_Down,
41 Qt.Key_Home,
42 Qt.Key_End,
43 Qt.Key_PageUp,
44 Qt.Key_PageDown,
46 return selection_move_keys
49 class Switcher(standard.Dialog):
50 """
51 Quick switcher base class. This contains input field, filter proxy model.
52 This will be inherited by outer-view class(SwitcherOuterView) or inner-view class
53 (SwitcherInnerView).
55 inner-view class is a quick-switcher widget including view. In this case, this
56 switcher will have switcher_list field, and show the items list in itself.
57 outer-view class is a quick-switcher widget without view(only input field), which
58 means sharing model with other view class.
60 switcher_selection_move signal is for the event that selection move key like UP,
61 DOWN has pressed while focusing on input field.
62 """
64 def __init__(
65 self,
66 context,
67 entries_model,
68 place_holder=None,
69 parent=None,
71 standard.Dialog.__init__(self, parent=parent)
73 self.context = context
74 self.entries_model = entries_model
76 self.filter_input = SwitcherLineEdit(place_holder=place_holder, parent=self)
78 self.proxy_model = SwitcherSortFilterProxyModel(entries_model, parent=self)
79 self.switcher_list = None
81 self.filter_input.textChanged.connect(self.filter_input_changed)
83 def filter_input_changed(self):
84 input_text = self.filter_input.text()
85 pattern = '.*'.join(re.escape(c) for c in input_text)
86 self.proxy_model.setFilterRegExp(pattern)
89 class SwitcherInnerView(Switcher):
90 def __init__(
91 self,
92 context,
93 entries_model,
94 title,
95 place_holder=None,
96 enter_action=None,
97 parent=None,
99 Switcher.__init__(
100 self,
101 context,
102 entries_model,
103 place_holder=place_holder,
104 parent=parent,
106 self.setWindowTitle(title)
107 if parent is not None:
108 self.setWindowModality(Qt.WindowModal)
110 self.enter_action = enter_action
111 self.switcher_list = SwitcherTreeView(
112 self.proxy_model, self.enter_selected_item, parent=self
115 self.main_layout = qtutils.vbox(
116 defs.no_margin, defs.spacing, self.filter_input, self.switcher_list
118 self.setLayout(self.main_layout)
120 # moving key has pressed while focusing on input field
121 self.filter_input.switcher_selection_move.connect(
122 self.switcher_list.keyPressEvent
124 # escape key pressed while focusing on input field
125 self.filter_input.switcher_escape.connect(self.close)
126 self.filter_input.switcher_accept.connect(self.accept_selected_item)
127 # some key except moving key has pressed while focusing on list view
128 self.switcher_list.switcher_inner_text.connect(self.filter_input.keyPressEvent)
130 # default selection for first index
131 first_proxy_idx = self.proxy_model.index(0, 0)
132 self.switcher_list.setCurrentIndex(first_proxy_idx)
134 self.set_initial_geometry(parent)
136 def accept_selected_item(self):
137 item = self.switcher_list.selected_item()
138 if item:
139 self.enter_action(item)
140 self.accept()
142 def set_initial_geometry(self, parent):
143 """Set the initial size and position"""
144 if parent is None:
145 return
146 # Size
147 width = parent.width()
148 height = parent.height()
149 self.resize(max(width * 2 // 3, 320), max(height * 2 // 3, 240))
151 def enter_selected_item(self, index):
152 item = self.switcher_list.model().itemFromIndex(index)
153 if item:
154 self.enter_action(item)
155 self.accept()
158 class SwitcherOuterView(Switcher):
159 def __init__(self, context, entries_model, place_holder=None, parent=None):
160 Switcher.__init__(
161 self,
162 context,
163 entries_model,
164 place_holder=place_holder,
165 parent=parent,
167 self.filter_input.hide()
169 self.main_layout = qtutils.vbox(defs.no_margin, defs.spacing, self.filter_input)
170 self.setLayout(self.main_layout)
172 def filter_input_changed(self):
173 super().filter_input_changed()
174 # Hide the input when it becomes empty.
175 input_text = self.filter_input.text()
176 if not input_text:
177 self.filter_input.hide()
180 class SwitcherLineEdit(text.LineEdit):
181 """Quick switcher input line class"""
183 # signal is for the event that selection move key like UP, DOWN has pressed
184 # while focusing on this line edit widget
185 switcher_selection_move = Signal(QtGui.QKeyEvent)
186 switcher_visible = Signal(bool)
187 switcher_accept = Signal()
188 switcher_escape = Signal()
190 def __init__(self, place_holder=None, parent=None):
191 text.LineEdit.__init__(self, parent=parent)
192 if place_holder:
193 self.setPlaceholderText(place_holder)
195 def keyPressEvent(self, event):
197 To be able to move the selection while focus on the input field, input text
198 field should be able to filter pressed key.
199 If pressed key is moving selection key like UP or DOWN, the
200 switcher_selection_move signal will be emitted and the view selection
201 will be moved regardless whether Switcher is inner-view or outer-view.
202 Or else, simply act like text input to the field.
204 selection_moving_keys = moving_keys()
205 pressed_key = event.key()
207 if pressed_key == Qt.Key_Escape:
208 self.switcher_escape.emit()
209 elif pressed_key in (Qt.Key_Enter, Qt.Key_Return):
210 self.switcher_accept.emit()
211 elif pressed_key in selection_moving_keys:
212 self.switcher_selection_move.emit(event)
213 else:
214 super().keyPressEvent(event)
217 class SwitcherSortFilterProxyModel(QtCore.QSortFilterProxyModel):
218 """Filtering class for candidate items."""
220 def __init__(self, entries, parent=None):
221 QtCore.QSortFilterProxyModel.__init__(self, parent)
223 self.entries = entries
225 self.setDynamicSortFilter(True)
226 self.setSourceModel(entries)
227 self.setFilterCaseSensitivity(Qt.CaseInsensitive)
229 def itemFromIndex(self, index):
230 return self.entries.itemFromIndex(self.mapToSource(index))
233 # pylint: disable=too-many-ancestors
234 class SwitcherTreeView(standard.TreeView):
235 """Tree view class for showing proxy items in SwitcherSortFilterProxyModel"""
237 # signal is for the event that some key except moving key has pressed
238 # while focusing this view
239 switcher_inner_text = Signal(QtGui.QKeyEvent)
241 def __init__(self, entries_proxy_model, enter_action, parent=None):
242 standard.TreeView.__init__(self, parent)
244 self.setHeaderHidden(True)
245 self.setModel(entries_proxy_model)
247 self.doubleClicked.connect(enter_action)
249 def keyPressEvent(self, event):
250 """hooks when a key has pressed while focusing on list view"""
251 selection_moving_keys = moving_keys()
252 pressed_key = event.key()
254 if pressed_key in selection_moving_keys or pressed_key == Qt.Key_Escape:
255 super().keyPressEvent(event)
256 else:
257 self.switcher_inner_text.emit(event)
260 class SwitcherListItem(QtGui.QStandardItem):
261 """Item class for SwitcherTreeView and SwitcherSortFilterProxyModel"""
263 def __init__(self, key, icon=None, name=None):
264 QtGui.QStandardItem.__init__(self)
266 self.key = key
267 if not name:
268 name = key
270 self.setText(name)
271 if icon:
272 self.setIcon(icon)