git-cola v4.3.1
[git-cola.git] / cola / widgets / switcher.py
blob1bae5d42ae735adbde673f3f124d6ff7b52fed44
1 """Provides quick switcher"""
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 import re
5 from qtpy import QtCore
6 from qtpy import QtGui
7 from qtpy.QtCore import Qt
8 from qtpy.QtCore import Signal
10 from .. import qtutils
11 from ..widgets import defs
12 from ..widgets import standard
13 from ..widgets import text
16 def switcher_inner_view(
17 context, entries, title=None, place_holder=None, enter_action=None, parent=None
19 dialog = SwitcherInnerView(
20 context, entries, title, place_holder, enter_action, parent
22 dialog.show()
23 return dialog
26 def switcher_outer_view(context, entries, place_holder=None, parent=None):
27 dialog = SwitcherOuterView(context, entries, place_holder, parent)
28 dialog.show()
29 return dialog
32 def switcher_item(key, icon=None, name=None):
33 return SwitcherListItem(key, icon, name)
36 def moving_keys():
37 selection_move_keys = [
38 Qt.Key_Enter,
39 Qt.Key_Return,
40 Qt.Key_Up,
41 Qt.Key_Down,
42 Qt.Key_Home,
43 Qt.Key_End,
44 Qt.Key_PageUp,
45 Qt.Key_PageDown,
47 return selection_move_keys
50 class Switcher(standard.Dialog):
51 """
52 Quick switcher base class. This contains input field, filter proxy model.
53 This will be inherited by outer-view class(SwitcherOuterView) or inner-view class
54 (SwitcherInnerView).
56 inner-view class is a quick-switcher widget including view. In this case, this
57 switcher will have switcher_list field, and show the items list in itself.
58 outer-view class is a quick-switcher widget without view(only input field), which
59 means sharing model with other view class.
61 switcher_selection_move signal is for the event that selection move key like UP,
62 DOWN has pressed while focusing on input field.
63 """
65 def __init__(
66 self,
67 context,
68 entries_model,
69 place_holder=None,
70 parent=None,
72 standard.Dialog.__init__(self, parent=parent)
74 self.context = context
75 self.entries_model = entries_model
77 self.filter_input = SwitcherLineEdit(place_holder=place_holder, parent=self)
79 self.proxy_model = SwitcherSortFilterProxyModel(entries_model, parent=self)
80 self.switcher_list = None
82 self.filter_input.textChanged.connect(self.filter_input_changed)
84 def filter_input_changed(self):
85 input_text = self.filter_input.text()
86 pattern = '.*'.join(re.escape(c) for c in input_text)
87 self.proxy_model.setFilterRegExp(pattern)
90 class SwitcherInnerView(Switcher):
91 def __init__(
92 self,
93 context,
94 entries_model,
95 title,
96 place_holder=None,
97 enter_action=None,
98 parent=None,
100 Switcher.__init__(
101 self,
102 context,
103 entries_model,
104 place_holder=place_holder,
105 parent=parent,
107 self.setWindowTitle(title)
108 if parent is not None:
109 self.setWindowModality(Qt.WindowModal)
111 self.enter_action = enter_action
112 self.switcher_list = SwitcherTreeView(
113 self.proxy_model, self.enter_selected_item, parent=self
116 self.main_layout = qtutils.vbox(
117 defs.no_margin, defs.spacing, self.filter_input, self.switcher_list
119 self.setLayout(self.main_layout)
121 # moving key has pressed while focusing on input field
122 self.filter_input.switcher_selection_move.connect(
123 self.switcher_list.keyPressEvent
125 # escape key pressed while focusing on input field
126 self.filter_input.switcher_escape.connect(self.close)
127 self.filter_input.switcher_accept.connect(self.accept_selected_item)
128 # some key except moving key has pressed while focusing on list view
129 self.switcher_list.switcher_inner_text.connect(self.filter_input.keyPressEvent)
131 # default selection for first index
132 first_proxy_idx = self.proxy_model.index(0, 0)
133 self.switcher_list.setCurrentIndex(first_proxy_idx)
135 self.set_initial_geometry(parent)
137 def accept_selected_item(self):
138 item = self.switcher_list.selected_item()
139 if item:
140 self.enter_action(item)
141 self.accept()
143 def set_initial_geometry(self, parent):
144 """Set the initial size and position"""
145 if parent is None:
146 return
147 # Size
148 width = parent.width()
149 height = parent.height()
150 self.resize(max(width * 2 // 3, 320), max(height * 2 // 3, 240))
152 def enter_selected_item(self, index):
153 item = self.switcher_list.model().itemFromIndex(index)
154 if item:
155 self.enter_action(item)
156 self.accept()
159 class SwitcherOuterView(Switcher):
160 def __init__(self, context, entries_model, place_holder=None, parent=None):
161 Switcher.__init__(
162 self,
163 context,
164 entries_model,
165 place_holder=place_holder,
166 parent=parent,
168 self.filter_input.hide()
170 self.main_layout = qtutils.vbox(defs.no_margin, defs.spacing, self.filter_input)
171 self.setLayout(self.main_layout)
173 def filter_input_changed(self):
174 super().filter_input_changed()
175 # Hide the input when it becomes empty.
176 input_text = self.filter_input.text()
177 if not input_text:
178 self.filter_input.hide()
181 class SwitcherLineEdit(text.LineEdit):
182 """Quick switcher input line class"""
184 # signal is for the event that selection move key like UP, DOWN has pressed
185 # while focusing on this line edit widget
186 switcher_selection_move = Signal(QtGui.QKeyEvent)
187 switcher_visible = Signal(bool)
188 switcher_accept = Signal()
189 switcher_escape = Signal()
191 def __init__(self, place_holder=None, parent=None):
192 text.LineEdit.__init__(self, parent=parent)
193 if place_holder:
194 self.setPlaceholderText(place_holder)
196 def keyPressEvent(self, event):
198 To be able to move the selection while focus on the input field, input text
199 field should be able to filter pressed key.
200 If pressed key is moving selection key like UP or DOWN, the
201 switcher_selection_move signal will be emitted and the view selection
202 will be moved regardless whether Switcher is inner-view or outer-view.
203 Or else, simply act like text input to the field.
205 selection_moving_keys = moving_keys()
206 pressed_key = event.key()
208 if pressed_key == Qt.Key_Escape:
209 self.switcher_escape.emit()
210 elif pressed_key in (Qt.Key_Enter, Qt.Key_Return):
211 self.switcher_accept.emit()
212 elif pressed_key in selection_moving_keys:
213 self.switcher_selection_move.emit(event)
214 else:
215 super().keyPressEvent(event)
218 class SwitcherSortFilterProxyModel(QtCore.QSortFilterProxyModel):
219 """Filtering class for candidate items."""
221 def __init__(self, entries, parent=None):
222 QtCore.QSortFilterProxyModel.__init__(self, parent)
224 self.entries = entries
226 self.setDynamicSortFilter(True)
227 self.setSourceModel(entries)
228 self.setFilterCaseSensitivity(Qt.CaseInsensitive)
230 def itemFromIndex(self, index):
231 return self.entries.itemFromIndex(self.mapToSource(index))
234 # pylint: disable=too-many-ancestors
235 class SwitcherTreeView(standard.TreeView):
236 """Tree view class for showing proxy items in SwitcherSortFilterProxyModel"""
238 # signal is for the event that some key except moving key has pressed
239 # while focusing this view
240 switcher_inner_text = Signal(QtGui.QKeyEvent)
242 def __init__(self, entries_proxy_model, enter_action, parent=None):
243 standard.TreeView.__init__(self, parent)
245 self.setHeaderHidden(True)
246 self.setModel(entries_proxy_model)
248 self.doubleClicked.connect(enter_action)
250 def keyPressEvent(self, event):
251 """hooks when a key has pressed while focusing on list view"""
252 selection_moving_keys = moving_keys()
253 pressed_key = event.key()
255 if pressed_key in selection_moving_keys or pressed_key == Qt.Key_Escape:
256 super().keyPressEvent(event)
257 else:
258 self.switcher_inner_text.emit(event)
261 class SwitcherListItem(QtGui.QStandardItem):
262 """Item class for SwitcherTreeView and SwitcherSortFilterProxyModel"""
264 def __init__(self, key, icon=None, name=None):
265 QtGui.QStandardItem.__init__(self)
267 self.key = key
268 if not name:
269 name = key
271 self.setText(name)
272 if icon:
273 self.setIcon(icon)