##// END OF EJS Templates
Updated file headers according to IPython Copyright Policy
Dimitry Kloper -
Show More
@@ -1,216 +1,218 b''
1 1 """MagicHelper - dockable widget showing magic commands for the MainWindow
2 2
3 3
4 4 Authors:
5 5
6 6 * Dimitry Kloper
7
8 7 """
9 8
9 # Copyright (c) IPython Development Team.
10 # Distributed under the terms of the Modified BSD License.
11
10 12 #-----------------------------------------------------------------------------
11 13 # Imports
12 14 #-----------------------------------------------------------------------------
13 15
14 16 # stdlib imports
15 17 import json
16 18 import re
17 19 import sys
18 20
19 21 # System library imports
20 22 from IPython.external.qt import QtGui,QtCore
21 23
22 24 from IPython.core.magic import magic_escapes
23 25
24 26 class MagicHelper(QtGui.QDockWidget):
25 27 """MagicHelper - dockable widget for convenient search and running of
26 28 magic command for IPython QtConsole.
27 29 """
28 30
29 31 #---------------------------------------------------------------------------
30 32 # signals
31 33 #---------------------------------------------------------------------------
32 34
33 35 pasteRequested = QtCore.pyqtSignal(str, name = 'pasteRequested')
34 36 """This signal is emitted when user wants to paste selected magic
35 37 command into the command line.
36 38 """
37 39
38 40 runRequested = QtCore.pyqtSignal(str, name = 'runRequested')
39 41 """This signal is emitted when user wants to execute selected magic command
40 42 """
41 43
42 44 readyForUpdate = QtCore.pyqtSignal(name = 'readyForUpdate')
43 45 """This signal is emitted when MagicHelper is ready to be populated.
44 46 Since kernel querying mechanisms are out of scope of this class,
45 47 it expects its owner to invoke MagicHelper.populate_magic_helper()
46 48 as a reaction on this event.
47 49 """
48 50
49 51 #---------------------------------------------------------------------------
50 52 # constructor
51 53 #---------------------------------------------------------------------------
52 54
53 55 def __init__(self, name, parent):
54 56 super(MagicHelper, self).__init__(name, parent)
55 57
56 58 self.data = None
57 59
58 60 class MinListWidget(QtGui.QListWidget):
59 61 """Temp class to overide the default QListWidget size hint
60 62 in order to make MagicHelper narrow
61 63 """
62 64 def sizeHint(self):
63 65 s = QtCore.QSize()
64 66 s.setHeight(super(MinListWidget,self).sizeHint().height())
65 67 s.setWidth(self.sizeHintForColumn(0))
66 68 return s
67 69
68 70 # construct content
69 71 self.frame = QtGui.QFrame()
70 72 self.search_label = QtGui.QLabel("Search:")
71 73 self.search_line = QtGui.QLineEdit()
72 74 self.search_class = QtGui.QComboBox()
73 75 self.search_list = MinListWidget()
74 76 self.paste_button = QtGui.QPushButton("Paste")
75 77 self.run_button = QtGui.QPushButton("Run")
76 78
77 79 # layout all the widgets
78 80 main_layout = QtGui.QVBoxLayout()
79 81 search_layout = QtGui.QHBoxLayout()
80 82 search_layout.addWidget(self.search_label)
81 83 search_layout.addWidget(self.search_line, 10)
82 84 main_layout.addLayout(search_layout)
83 85 main_layout.addWidget(self.search_class)
84 86 main_layout.addWidget(self.search_list, 10)
85 87 action_layout = QtGui.QHBoxLayout()
86 88 action_layout.addWidget(self.paste_button)
87 89 action_layout.addWidget(self.run_button)
88 90 main_layout.addLayout(action_layout)
89 91
90 92 self.frame.setLayout(main_layout)
91 93 self.setWidget(self.frame)
92 94
93 95 # connect all the relevant signals to handlers
94 96 self.visibilityChanged[bool].connect( self._update_magic_helper )
95 97 self.search_class.activated[int].connect(
96 98 self.class_selected
97 99 )
98 100 self.search_line.textChanged[str].connect(
99 101 self.search_changed
100 102 )
101 103 self.search_list.itemDoubleClicked[QtGui.QListWidgetItem].connect(
102 104 self.paste_requested
103 105 )
104 106 self.paste_button.clicked[bool].connect(
105 107 self.paste_requested
106 108 )
107 109 self.run_button.clicked[bool].connect(
108 110 self.run_requested
109 111 )
110 112
111 113 #---------------------------------------------------------------------------
112 114 # implementation
113 115 #---------------------------------------------------------------------------
114 116
115 117 def _update_magic_helper(self, visible):
116 118 """Start update sequence.
117 119 This method is called when MagicHelper becomes visible. It clears
118 120 the content and emits readyForUpdate signal. The owner of the
119 121 instance is expected to invoke populate_magic_helper() when magic
120 122 info is available.
121 123 """
122 124 if not visible or self.data != None:
123 125 return
124 126 self.data = {}
125 127 self.search_class.clear()
126 128 self.search_class.addItem("Populating...")
127 129 self.search_list.clear()
128 130 self.readyForUpdate.emit()
129 131
130 132 def populate_magic_helper(self, data):
131 133 """Expects data returned by lsmagics query from kernel.
132 134 Populates the search_class and search_list with relevant items.
133 135 """
134 136 self.search_class.clear()
135 137 self.search_list.clear()
136 138
137 139 self.data = json.loads(
138 140 data['data'].get('application/json', {})
139 141 )
140 142
141 143 self.search_class.addItem('All Magics', 'any')
142 144 classes = set()
143 145
144 146 for mtype in sorted(self.data):
145 147 subdict = self.data[mtype]
146 148 for name in sorted(subdict):
147 149 classes.add(subdict[name])
148 150
149 151 for cls in sorted(classes):
150 152 label = re.sub("([a-zA-Z]+)([A-Z][a-z])","\g<1> \g<2>", cls)
151 153 self.search_class.addItem(label, cls)
152 154
153 155 self.filter_magic_helper('.', 'any')
154 156
155 157 def class_selected(self, index):
156 158 """Handle search_class selection changes
157 159 """
158 160 item = self.search_class.itemData(index)
159 161 regex = self.search_line.text()
160 162 self.filter_magic_helper(regex = regex, cls = item)
161 163
162 164 def search_changed(self, search_string):
163 165 """Handle search_line text changes.
164 166 The text is interpreted as a regular expression
165 167 """
166 168 item = self.search_class.itemData(
167 169 self.search_class.currentIndex()
168 170 )
169 171 self.filter_magic_helper(regex = search_string, cls = item)
170 172
171 173 def _get_current_search_item(self, item = None):
172 174 """Retrieve magic command currently selected in the search_list
173 175 """
174 176 text = None
175 177 if not isinstance(item, QtGui.QListWidgetItem):
176 178 item = self.search_list.currentItem()
177 179 text = item.text()
178 180 return text
179 181
180 182 def paste_requested(self, item = None):
181 183 """Emit pasteRequested signal with currently selected item text
182 184 """
183 185 text = self._get_current_search_item(item)
184 186 if text != None:
185 187 self.pasteRequested.emit(text)
186 188
187 189 def run_requested(self, item = None):
188 190 """Emit runRequested signal with currently selected item text
189 191 """
190 192 text = self._get_current_search_item(item)
191 193 if text != None:
192 194 self.runRequested.emit(text)
193 195
194 196 def filter_magic_helper(self, regex, cls):
195 197 """Update search_list with magic commands whose text match
196 198 regex and class match cls.
197 199 If cls equals 'any' - any class matches.
198 200 """
199 201 if regex == "" or regex == None:
200 202 regex = '.'
201 203 if cls == None:
202 204 cls = 'any'
203 205
204 206 self.search_list.clear()
205 207 for mtype in sorted(self.data):
206 208 subdict = self.data[mtype]
207 209 prefix = magic_escapes[mtype]
208 210
209 211 for name in sorted(subdict):
210 212 mclass = subdict[name]
211 213 pmagic = prefix + name
212 214
213 215 if (re.match(regex, name) or re.match(regex, pmagic)) and \
214 216 (cls == 'any' or cls == mclass):
215 217 self.search_list.addItem(pmagic)
216 218
@@ -1,934 +1,938 b''
1 1 """The Qt MainWindow for the QtConsole
2 2
3 3 This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for
4 4 common actions.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14 * Paul Ivanov
15 15
16 16 """
17 17
18 # Copyright (c) IPython Development Team.
19 # Distributed under the terms of the Modified BSD License.
20
21
18 22 #-----------------------------------------------------------------------------
19 23 # Imports
20 24 #-----------------------------------------------------------------------------
21 25
22 26 # stdlib imports
23 27 import json
24 28 import re
25 29 import sys
26 30 import webbrowser
27 31 from threading import Thread
28 32
29 33 # System library imports
30 34 from IPython.external.qt import QtGui,QtCore
31 35
32 36 from IPython.core.magic import magic_escapes
33 37
34 38 def background(f):
35 39 """call a function in a simple thread, to prevent blocking"""
36 40 t = Thread(target=f)
37 41 t.start()
38 42 return t
39 43
40 44 #-----------------------------------------------------------------------------
41 45 # Classes
42 46 #-----------------------------------------------------------------------------
43 47
44 48 class MainWindow(QtGui.QMainWindow):
45 49
46 50 #---------------------------------------------------------------------------
47 51 # 'object' interface
48 52 #---------------------------------------------------------------------------
49 53
50 54 def __init__(self, app,
51 55 confirm_exit=True,
52 56 new_frontend_factory=None, slave_frontend_factory=None,
53 57 ):
54 58 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
55 59
56 60 Parameters
57 61 ----------
58 62
59 63 app : reference to QApplication parent
60 64 confirm_exit : bool, optional
61 65 Whether we should prompt on close of tabs
62 66 new_frontend_factory : callable
63 67 A callable that returns a new IPythonWidget instance, attached to
64 68 its own running kernel.
65 69 slave_frontend_factory : callable
66 70 A callable that takes an existing IPythonWidget, and returns a new
67 71 IPythonWidget instance, attached to the same kernel.
68 72 """
69 73
70 74 super(MainWindow, self).__init__()
71 75 self._kernel_counter = 0
72 76 self._app = app
73 77 self.confirm_exit = confirm_exit
74 78 self.new_frontend_factory = new_frontend_factory
75 79 self.slave_frontend_factory = slave_frontend_factory
76 80
77 81 self.tab_widget = QtGui.QTabWidget(self)
78 82 self.tab_widget.setDocumentMode(True)
79 83 self.tab_widget.setTabsClosable(True)
80 84 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
81 85
82 86 self.setCentralWidget(self.tab_widget)
83 87 # hide tab bar at first, since we have no tabs:
84 88 self.tab_widget.tabBar().setVisible(False)
85 89 # prevent focus in tab bar
86 90 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
87 91
88 92 def update_tab_bar_visibility(self):
89 93 """ update visibility of the tabBar depending of the number of tab
90 94
91 95 0 or 1 tab, tabBar hidden
92 96 2+ tabs, tabBar visible
93 97
94 98 send a self.close if number of tab ==0
95 99
96 100 need to be called explicitly, or be connected to tabInserted/tabRemoved
97 101 """
98 102 if self.tab_widget.count() <= 1:
99 103 self.tab_widget.tabBar().setVisible(False)
100 104 else:
101 105 self.tab_widget.tabBar().setVisible(True)
102 106 if self.tab_widget.count()==0 :
103 107 self.close()
104 108
105 109 @property
106 110 def next_kernel_id(self):
107 111 """constantly increasing counter for kernel IDs"""
108 112 c = self._kernel_counter
109 113 self._kernel_counter += 1
110 114 return c
111 115
112 116 @property
113 117 def active_frontend(self):
114 118 return self.tab_widget.currentWidget()
115 119
116 120 def create_tab_with_new_frontend(self):
117 121 """create a new frontend and attach it to a new tab"""
118 122 widget = self.new_frontend_factory()
119 123 self.add_tab_with_frontend(widget)
120 124
121 125 def create_tab_with_current_kernel(self):
122 126 """create a new frontend attached to the same kernel as the current tab"""
123 127 current_widget = self.tab_widget.currentWidget()
124 128 current_widget_index = self.tab_widget.indexOf(current_widget)
125 129 current_widget_name = self.tab_widget.tabText(current_widget_index)
126 130 widget = self.slave_frontend_factory(current_widget)
127 131 if 'slave' in current_widget_name:
128 132 # don't keep stacking slaves
129 133 name = current_widget_name
130 134 else:
131 135 name = '(%s) slave' % current_widget_name
132 136 self.add_tab_with_frontend(widget,name=name)
133 137
134 138 def close_tab(self,current_tab):
135 139 """ Called when you need to try to close a tab.
136 140
137 141 It takes the number of the tab to be closed as argument, or a reference
138 142 to the widget inside this tab
139 143 """
140 144
141 145 # let's be sure "tab" and "closing widget" are respectively the index
142 146 # of the tab to close and a reference to the frontend to close
143 147 if type(current_tab) is not int :
144 148 current_tab = self.tab_widget.indexOf(current_tab)
145 149 closing_widget=self.tab_widget.widget(current_tab)
146 150
147 151
148 152 # when trying to be closed, widget might re-send a request to be
149 153 # closed again, but will be deleted when event will be processed. So
150 154 # need to check that widget still exists and skip if not. One example
151 155 # of this is when 'exit' is sent in a slave tab. 'exit' will be
152 156 # re-sent by this function on the master widget, which ask all slave
153 157 # widgets to exit
154 158 if closing_widget==None:
155 159 return
156 160
157 161 #get a list of all slave widgets on the same kernel.
158 162 slave_tabs = self.find_slave_widgets(closing_widget)
159 163
160 164 keepkernel = None #Use the prompt by default
161 165 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
162 166 keepkernel = closing_widget._keep_kernel_on_exit
163 167 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
164 168 # we set local slave tabs._hidden to True to avoid prompting for kernel
165 169 # restart when they get the signal. and then "forward" the 'exit'
166 170 # to the main window
167 171 if keepkernel is not None:
168 172 for tab in slave_tabs:
169 173 tab._hidden = True
170 174 if closing_widget in slave_tabs:
171 175 try :
172 176 self.find_master_tab(closing_widget).execute('exit')
173 177 except AttributeError:
174 178 self.log.info("Master already closed or not local, closing only current tab")
175 179 self.tab_widget.removeTab(current_tab)
176 180 self.update_tab_bar_visibility()
177 181 return
178 182
179 183 kernel_client = closing_widget.kernel_client
180 184 kernel_manager = closing_widget.kernel_manager
181 185
182 186 if keepkernel is None and not closing_widget._confirm_exit:
183 187 # don't prompt, just terminate the kernel if we own it
184 188 # or leave it alone if we don't
185 189 keepkernel = closing_widget._existing
186 190 if keepkernel is None: #show prompt
187 191 if kernel_client and kernel_client.channels_running:
188 192 title = self.window().windowTitle()
189 193 cancel = QtGui.QMessageBox.Cancel
190 194 okay = QtGui.QMessageBox.Ok
191 195 if closing_widget._may_close:
192 196 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
193 197 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
194 198 justthis = QtGui.QPushButton("&No, just this Tab", self)
195 199 justthis.setShortcut('N')
196 200 closeall = QtGui.QPushButton("&Yes, close all", self)
197 201 closeall.setShortcut('Y')
198 202 # allow ctrl-d ctrl-d exit, like in terminal
199 203 closeall.setShortcut('Ctrl+D')
200 204 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
201 205 title, msg)
202 206 box.setInformativeText(info)
203 207 box.addButton(cancel)
204 208 box.addButton(justthis, QtGui.QMessageBox.NoRole)
205 209 box.addButton(closeall, QtGui.QMessageBox.YesRole)
206 210 box.setDefaultButton(closeall)
207 211 box.setEscapeButton(cancel)
208 212 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
209 213 box.setIconPixmap(pixmap)
210 214 reply = box.exec_()
211 215 if reply == 1: # close All
212 216 for slave in slave_tabs:
213 217 background(slave.kernel_client.stop_channels)
214 218 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
215 219 closing_widget.execute("exit")
216 220 self.tab_widget.removeTab(current_tab)
217 221 background(kernel_client.stop_channels)
218 222 elif reply == 0: # close Console
219 223 if not closing_widget._existing:
220 224 # Have kernel: don't quit, just close the tab
221 225 closing_widget.execute("exit True")
222 226 self.tab_widget.removeTab(current_tab)
223 227 background(kernel_client.stop_channels)
224 228 else:
225 229 reply = QtGui.QMessageBox.question(self, title,
226 230 "Are you sure you want to close this Console?"+
227 231 "\nThe Kernel and other Consoles will remain active.",
228 232 okay|cancel,
229 233 defaultButton=okay
230 234 )
231 235 if reply == okay:
232 236 self.tab_widget.removeTab(current_tab)
233 237 elif keepkernel: #close console but leave kernel running (no prompt)
234 238 self.tab_widget.removeTab(current_tab)
235 239 background(kernel_client.stop_channels)
236 240 else: #close console and kernel (no prompt)
237 241 self.tab_widget.removeTab(current_tab)
238 242 if kernel_client and kernel_client.channels_running:
239 243 for slave in slave_tabs:
240 244 background(slave.kernel_client.stop_channels)
241 245 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
242 246 if kernel_manager:
243 247 kernel_manager.shutdown_kernel()
244 248 background(kernel_client.stop_channels)
245 249
246 250 self.update_tab_bar_visibility()
247 251
248 252 def add_tab_with_frontend(self,frontend,name=None):
249 253 """ insert a tab with a given frontend in the tab bar, and give it a name
250 254
251 255 """
252 256 if not name:
253 257 name = 'kernel %i' % self.next_kernel_id
254 258 self.tab_widget.addTab(frontend,name)
255 259 self.update_tab_bar_visibility()
256 260 self.make_frontend_visible(frontend)
257 261 frontend.exit_requested.connect(self.close_tab)
258 262
259 263 def next_tab(self):
260 264 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
261 265
262 266 def prev_tab(self):
263 267 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
264 268
265 269 def make_frontend_visible(self,frontend):
266 270 widget_index=self.tab_widget.indexOf(frontend)
267 271 if widget_index > 0 :
268 272 self.tab_widget.setCurrentIndex(widget_index)
269 273
270 274 def find_master_tab(self,tab,as_list=False):
271 275 """
272 276 Try to return the frontend that owns the kernel attached to the given widget/tab.
273 277
274 278 Only finds frontend owned by the current application. Selection
275 279 based on port of the kernel might be inaccurate if several kernel
276 280 on different ip use same port number.
277 281
278 282 This function does the conversion tabNumber/widget if needed.
279 283 Might return None if no master widget (non local kernel)
280 284 Will crash IPython if more than 1 masterWidget
281 285
282 286 When asList set to True, always return a list of widget(s) owning
283 287 the kernel. The list might be empty or containing several Widget.
284 288 """
285 289
286 290 #convert from/to int/richIpythonWidget if needed
287 291 if isinstance(tab, int):
288 292 tab = self.tab_widget.widget(tab)
289 293 km=tab.kernel_client
290 294
291 295 #build list of all widgets
292 296 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
293 297
294 298 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
295 299 # And should have a _may_close attribute
296 300 filtered_widget_list = [ widget for widget in widget_list if
297 301 widget.kernel_client.connection_file == km.connection_file and
298 302 hasattr(widget,'_may_close') ]
299 303 # the master widget is the one that may close the kernel
300 304 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
301 305 if as_list:
302 306 return master_widget
303 307 assert(len(master_widget)<=1 )
304 308 if len(master_widget)==0:
305 309 return None
306 310
307 311 return master_widget[0]
308 312
309 313 def find_slave_widgets(self,tab):
310 314 """return all the frontends that do not own the kernel attached to the given widget/tab.
311 315
312 316 Only find frontends owned by the current application. Selection
313 317 based on connection file of the kernel.
314 318
315 319 This function does the conversion tabNumber/widget if needed.
316 320 """
317 321 #convert from/to int/richIpythonWidget if needed
318 322 if isinstance(tab, int):
319 323 tab = self.tab_widget.widget(tab)
320 324 km=tab.kernel_client
321 325
322 326 #build list of all widgets
323 327 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
324 328
325 329 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
326 330 filtered_widget_list = ( widget for widget in widget_list if
327 331 widget.kernel_client.connection_file == km.connection_file)
328 332 # Get a list of all widget owning the same kernel and removed it from
329 333 # the previous cadidate. (better using sets ?)
330 334 master_widget_list = self.find_master_tab(tab, as_list=True)
331 335 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
332 336
333 337 return slave_list
334 338
335 339 # Populate the menu bar with common actions and shortcuts
336 340 def add_menu_action(self, menu, action, defer_shortcut=False):
337 341 """Add action to menu as well as self
338 342
339 343 So that when the menu bar is invisible, its actions are still available.
340 344
341 345 If defer_shortcut is True, set the shortcut context to widget-only,
342 346 where it will avoid conflict with shortcuts already bound to the
343 347 widgets themselves.
344 348 """
345 349 menu.addAction(action)
346 350 self.addAction(action)
347 351
348 352 if defer_shortcut:
349 353 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
350 354
351 355 def init_menu_bar(self):
352 356 #create menu in the order they should appear in the menu bar
353 357 self.init_file_menu()
354 358 self.init_edit_menu()
355 359 self.init_view_menu()
356 360 self.init_kernel_menu()
357 361 self.init_magic_menu()
358 362 self.init_window_menu()
359 363 self.init_help_menu()
360 364
361 365 def init_file_menu(self):
362 366 self.file_menu = self.menuBar().addMenu("&File")
363 367
364 368 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
365 369 self,
366 370 shortcut="Ctrl+T",
367 371 triggered=self.create_tab_with_new_frontend)
368 372 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
369 373
370 374 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
371 375 self,
372 376 shortcut="Ctrl+Shift+T",
373 377 triggered=self.create_tab_with_current_kernel)
374 378 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
375 379
376 380 self.file_menu.addSeparator()
377 381
378 382 self.close_action=QtGui.QAction("&Close Tab",
379 383 self,
380 384 shortcut=QtGui.QKeySequence.Close,
381 385 triggered=self.close_active_frontend
382 386 )
383 387 self.add_menu_action(self.file_menu, self.close_action)
384 388
385 389 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
386 390 self,
387 391 shortcut=QtGui.QKeySequence.Save,
388 392 triggered=self.export_action_active_frontend
389 393 )
390 394 self.add_menu_action(self.file_menu, self.export_action, True)
391 395
392 396 self.file_menu.addSeparator()
393 397
394 398 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
395 399 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
396 400 # Only override the default if there is a collision.
397 401 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
398 402 printkey = "Ctrl+Shift+P"
399 403 self.print_action = QtGui.QAction("&Print",
400 404 self,
401 405 shortcut=printkey,
402 406 triggered=self.print_action_active_frontend)
403 407 self.add_menu_action(self.file_menu, self.print_action, True)
404 408
405 409 if sys.platform != 'darwin':
406 410 # OSX always has Quit in the Application menu, only add it
407 411 # to the File menu elsewhere.
408 412
409 413 self.file_menu.addSeparator()
410 414
411 415 self.quit_action = QtGui.QAction("&Quit",
412 416 self,
413 417 shortcut=QtGui.QKeySequence.Quit,
414 418 triggered=self.close,
415 419 )
416 420 self.add_menu_action(self.file_menu, self.quit_action)
417 421
418 422
419 423 def init_edit_menu(self):
420 424 self.edit_menu = self.menuBar().addMenu("&Edit")
421 425
422 426 self.undo_action = QtGui.QAction("&Undo",
423 427 self,
424 428 shortcut=QtGui.QKeySequence.Undo,
425 429 statusTip="Undo last action if possible",
426 430 triggered=self.undo_active_frontend
427 431 )
428 432 self.add_menu_action(self.edit_menu, self.undo_action)
429 433
430 434 self.redo_action = QtGui.QAction("&Redo",
431 435 self,
432 436 shortcut=QtGui.QKeySequence.Redo,
433 437 statusTip="Redo last action if possible",
434 438 triggered=self.redo_active_frontend)
435 439 self.add_menu_action(self.edit_menu, self.redo_action)
436 440
437 441 self.edit_menu.addSeparator()
438 442
439 443 self.cut_action = QtGui.QAction("&Cut",
440 444 self,
441 445 shortcut=QtGui.QKeySequence.Cut,
442 446 triggered=self.cut_active_frontend
443 447 )
444 448 self.add_menu_action(self.edit_menu, self.cut_action, True)
445 449
446 450 self.copy_action = QtGui.QAction("&Copy",
447 451 self,
448 452 shortcut=QtGui.QKeySequence.Copy,
449 453 triggered=self.copy_active_frontend
450 454 )
451 455 self.add_menu_action(self.edit_menu, self.copy_action, True)
452 456
453 457 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
454 458 self,
455 459 shortcut="Ctrl+Shift+C",
456 460 triggered=self.copy_raw_active_frontend
457 461 )
458 462 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
459 463
460 464 self.paste_action = QtGui.QAction("&Paste",
461 465 self,
462 466 shortcut=QtGui.QKeySequence.Paste,
463 467 triggered=self.paste_active_frontend
464 468 )
465 469 self.add_menu_action(self.edit_menu, self.paste_action, True)
466 470
467 471 self.edit_menu.addSeparator()
468 472
469 473 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
470 474 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
471 475 # Only override the default if there is a collision.
472 476 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
473 477 selectall = "Ctrl+Shift+A"
474 478 self.select_all_action = QtGui.QAction("Select &All",
475 479 self,
476 480 shortcut=selectall,
477 481 triggered=self.select_all_active_frontend
478 482 )
479 483 self.add_menu_action(self.edit_menu, self.select_all_action, True)
480 484
481 485
482 486 def init_view_menu(self):
483 487 self.view_menu = self.menuBar().addMenu("&View")
484 488
485 489 if sys.platform != 'darwin':
486 490 # disable on OSX, where there is always a menu bar
487 491 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
488 492 self,
489 493 shortcut="Ctrl+Shift+M",
490 494 statusTip="Toggle visibility of menubar",
491 495 triggered=self.toggle_menu_bar)
492 496 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
493 497
494 498 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
495 499 self.full_screen_act = QtGui.QAction("&Full Screen",
496 500 self,
497 501 shortcut=fs_key,
498 502 statusTip="Toggle between Fullscreen and Normal Size",
499 503 triggered=self.toggleFullScreen)
500 504 self.add_menu_action(self.view_menu, self.full_screen_act)
501 505
502 506 self.view_menu.addSeparator()
503 507
504 508 self.increase_font_size = QtGui.QAction("Zoom &In",
505 509 self,
506 510 shortcut=QtGui.QKeySequence.ZoomIn,
507 511 triggered=self.increase_font_size_active_frontend
508 512 )
509 513 self.add_menu_action(self.view_menu, self.increase_font_size, True)
510 514
511 515 self.decrease_font_size = QtGui.QAction("Zoom &Out",
512 516 self,
513 517 shortcut=QtGui.QKeySequence.ZoomOut,
514 518 triggered=self.decrease_font_size_active_frontend
515 519 )
516 520 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
517 521
518 522 self.reset_font_size = QtGui.QAction("Zoom &Reset",
519 523 self,
520 524 shortcut="Ctrl+0",
521 525 triggered=self.reset_font_size_active_frontend
522 526 )
523 527 self.add_menu_action(self.view_menu, self.reset_font_size, True)
524 528
525 529 self.view_menu.addSeparator()
526 530
527 531 self.clear_action = QtGui.QAction("&Clear Screen",
528 532 self,
529 533 shortcut='Ctrl+L',
530 534 statusTip="Clear the console",
531 535 triggered=self.clear_magic_active_frontend)
532 536 self.add_menu_action(self.view_menu, self.clear_action)
533 537
534 538 self.pager_menu = self.view_menu.addMenu("&Pager")
535 539
536 540 hsplit_action = QtGui.QAction(".. &Horizontal Split",
537 541 self,
538 542 triggered=lambda: self.set_paging_active_frontend('hsplit'))
539 543
540 544 vsplit_action = QtGui.QAction(" : &Vertical Split",
541 545 self,
542 546 triggered=lambda: self.set_paging_active_frontend('vsplit'))
543 547
544 548 inside_action = QtGui.QAction(" &Inside Pager",
545 549 self,
546 550 triggered=lambda: self.set_paging_active_frontend('inside'))
547 551
548 552 self.pager_menu.addAction(hsplit_action)
549 553 self.pager_menu.addAction(vsplit_action)
550 554 self.pager_menu.addAction(inside_action)
551 555
552 556 def init_kernel_menu(self):
553 557 self.kernel_menu = self.menuBar().addMenu("&Kernel")
554 558 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
555 559 # keep the signal shortcuts to ctrl, rather than
556 560 # platform-default like we do elsewhere.
557 561
558 562 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
559 563
560 564 self.interrupt_kernel_action = QtGui.QAction("&Interrupt current Kernel",
561 565 self,
562 566 triggered=self.interrupt_kernel_active_frontend,
563 567 shortcut=ctrl+"+C",
564 568 )
565 569 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
566 570
567 571 self.restart_kernel_action = QtGui.QAction("&Restart current Kernel",
568 572 self,
569 573 triggered=self.restart_kernel_active_frontend,
570 574 shortcut=ctrl+"+.",
571 575 )
572 576 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
573 577
574 578 self.kernel_menu.addSeparator()
575 579
576 580 self.confirm_restart_kernel_action = QtGui.QAction("&Confirm kernel restart",
577 581 self,
578 582 checkable=True,
579 583 checked=self.active_frontend.confirm_restart,
580 584 triggered=self.toggle_confirm_restart_active_frontend
581 585 )
582 586
583 587 self.add_menu_action(self.kernel_menu, self.confirm_restart_kernel_action)
584 588 self.tab_widget.currentChanged.connect(self.update_restart_checkbox)
585 589
586 590 def init_magic_menu(self):
587 591 self.magic_menu = self.menuBar().addMenu("&Magic")
588 592
589 593 self.add_menu_action(self.magic_menu,
590 594 self.magic_helper.toggleViewAction())
591 595
592 596 self.magic_menu_separator = self.magic_menu.addSeparator()
593 597
594 598 self.reset_action = QtGui.QAction("&Reset",
595 599 self,
596 600 statusTip="Clear all variables from workspace",
597 601 triggered=self.reset_magic_active_frontend)
598 602 self.add_menu_action(self.magic_menu, self.reset_action)
599 603
600 604 self.history_action = QtGui.QAction("&History",
601 605 self,
602 606 statusTip="show command history",
603 607 triggered=self.history_magic_active_frontend)
604 608 self.add_menu_action(self.magic_menu, self.history_action)
605 609
606 610 self.save_action = QtGui.QAction("E&xport History ",
607 611 self,
608 612 statusTip="Export History as Python File",
609 613 triggered=self.save_magic_active_frontend)
610 614 self.add_menu_action(self.magic_menu, self.save_action)
611 615
612 616 self.who_action = QtGui.QAction("&Who",
613 617 self,
614 618 statusTip="List interactive variables",
615 619 triggered=self.who_magic_active_frontend)
616 620 self.add_menu_action(self.magic_menu, self.who_action)
617 621
618 622 self.who_ls_action = QtGui.QAction("Wh&o ls",
619 623 self,
620 624 statusTip="Return a list of interactive variables",
621 625 triggered=self.who_ls_magic_active_frontend)
622 626 self.add_menu_action(self.magic_menu, self.who_ls_action)
623 627
624 628 self.whos_action = QtGui.QAction("Who&s",
625 629 self,
626 630 statusTip="List interactive variables with details",
627 631 triggered=self.whos_magic_active_frontend)
628 632 self.add_menu_action(self.magic_menu, self.whos_action)
629 633
630 634 def init_window_menu(self):
631 635 self.window_menu = self.menuBar().addMenu("&Window")
632 636 if sys.platform == 'darwin':
633 637 # add min/maximize actions to OSX, which lacks default bindings.
634 638 self.minimizeAct = QtGui.QAction("Mini&mize",
635 639 self,
636 640 shortcut="Ctrl+m",
637 641 statusTip="Minimize the window/Restore Normal Size",
638 642 triggered=self.toggleMinimized)
639 643 # maximize is called 'Zoom' on OSX for some reason
640 644 self.maximizeAct = QtGui.QAction("&Zoom",
641 645 self,
642 646 shortcut="Ctrl+Shift+M",
643 647 statusTip="Maximize the window/Restore Normal Size",
644 648 triggered=self.toggleMaximized)
645 649
646 650 self.add_menu_action(self.window_menu, self.minimizeAct)
647 651 self.add_menu_action(self.window_menu, self.maximizeAct)
648 652 self.window_menu.addSeparator()
649 653
650 654 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
651 655 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
652 656 self,
653 657 shortcut=prev_key,
654 658 statusTip="Select previous tab",
655 659 triggered=self.prev_tab)
656 660 self.add_menu_action(self.window_menu, self.prev_tab_act)
657 661
658 662 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
659 663 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
660 664 self,
661 665 shortcut=next_key,
662 666 statusTip="Select next tab",
663 667 triggered=self.next_tab)
664 668 self.add_menu_action(self.window_menu, self.next_tab_act)
665 669
666 670 def init_help_menu(self):
667 671 # please keep the Help menu in Mac Os even if empty. It will
668 672 # automatically contain a search field to search inside menus and
669 673 # please keep it spelled in English, as long as Qt Doesn't support
670 674 # a QAction.MenuRole like HelpMenuRole otherwise it will lose
671 675 # this search field functionality
672 676
673 677 self.help_menu = self.menuBar().addMenu("&Help")
674 678
675 679
676 680 # Help Menu
677 681
678 682 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
679 683 self,
680 684 triggered=self.intro_active_frontend
681 685 )
682 686 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
683 687
684 688 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
685 689 self,
686 690 triggered=self.quickref_active_frontend
687 691 )
688 692 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
689 693
690 694 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
691 695 self,
692 696 triggered=self.guiref_active_frontend
693 697 )
694 698 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
695 699
696 700 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
697 701 self,
698 702 triggered=self._open_online_help)
699 703 self.add_menu_action(self.help_menu, self.onlineHelpAct)
700 704
701 705 def init_magic_helper(self):
702 706 from .magic_helper import MagicHelper
703 707
704 708 self.magic_helper = MagicHelper("Show Magics", self)
705 709
706 710 self.magic_helper.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
707 711 QtCore.Qt.RightDockWidgetArea)
708 712 self.magic_helper.setVisible(False)
709 713
710 714 self.magic_helper.pasteRequested[str].connect(
711 715 self.magic_helper_paste_requested
712 716 )
713 717 self.magic_helper.runRequested[str].connect(
714 718 self.magic_helper_run_requested
715 719 )
716 720 self.magic_helper.readyForUpdate.connect(
717 721 self.magic_helper_update_requested
718 722 )
719 723
720 724 self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.magic_helper)
721 725
722 726 def _set_active_frontend_focus(self):
723 727 # this is a hack, self.active_frontend._control seems to be
724 728 # a private member. Unfortunately this is the only method
725 729 # to set focus reliably
726 730 QtCore.QTimer.singleShot(200, self.active_frontend._control.setFocus)
727 731
728 732 def magic_helper_paste_requested(self, text = None):
729 733 if text != None:
730 734 self.active_frontend.input_buffer = text
731 735 self._set_active_frontend_focus()
732 736
733 737 def magic_helper_run_requested(self, text = None):
734 738 if text != None:
735 739 self.active_frontend.execute(text)
736 740 self._set_active_frontend_focus()
737 741
738 742 def magic_helper_update_requested(self):
739 743 def _handle_data(data):
740 744 if not data:
741 745 return
742 746
743 747 if data['status'] != 'ok':
744 748 self.log.warn(
745 749 "%%lsmagic user-expression failed: {}".format(data)
746 750 )
747 751 return
748 752 self.magic_helper.populate_magic_helper(data)
749 753
750 754 self.active_frontend._silent_exec_callback(
751 755 'get_ipython().magic("lsmagic")',
752 756 _handle_data
753 757 )
754 758
755 759 # minimize/maximize/fullscreen actions:
756 760
757 761 def toggle_menu_bar(self):
758 762 menu_bar = self.menuBar()
759 763 if menu_bar.isVisible():
760 764 menu_bar.setVisible(False)
761 765 else:
762 766 menu_bar.setVisible(True)
763 767
764 768 def toggleMinimized(self):
765 769 if not self.isMinimized():
766 770 self.showMinimized()
767 771 else:
768 772 self.showNormal()
769 773
770 774 def _open_online_help(self):
771 775 filename="http://ipython.org/ipython-doc/stable/index.html"
772 776 webbrowser.open(filename, new=1, autoraise=True)
773 777
774 778 def toggleMaximized(self):
775 779 if not self.isMaximized():
776 780 self.showMaximized()
777 781 else:
778 782 self.showNormal()
779 783
780 784 # Min/Max imizing while in full screen give a bug
781 785 # when going out of full screen, at least on OSX
782 786 def toggleFullScreen(self):
783 787 if not self.isFullScreen():
784 788 self.showFullScreen()
785 789 if sys.platform == 'darwin':
786 790 self.maximizeAct.setEnabled(False)
787 791 self.minimizeAct.setEnabled(False)
788 792 else:
789 793 self.showNormal()
790 794 if sys.platform == 'darwin':
791 795 self.maximizeAct.setEnabled(True)
792 796 self.minimizeAct.setEnabled(True)
793 797
794 798 def set_paging_active_frontend(self, paging):
795 799 self.active_frontend._set_paging(paging)
796 800
797 801 def close_active_frontend(self):
798 802 self.close_tab(self.active_frontend)
799 803
800 804 def restart_kernel_active_frontend(self):
801 805 self.active_frontend.request_restart_kernel()
802 806
803 807 def interrupt_kernel_active_frontend(self):
804 808 self.active_frontend.request_interrupt_kernel()
805 809
806 810 def toggle_confirm_restart_active_frontend(self):
807 811 widget = self.active_frontend
808 812 widget.confirm_restart = not widget.confirm_restart
809 813 self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
810 814
811 815 def update_restart_checkbox(self):
812 816 if self.active_frontend is None:
813 817 return
814 818 widget = self.active_frontend
815 819 self.confirm_restart_kernel_action.setChecked(widget.confirm_restart)
816 820
817 821 def cut_active_frontend(self):
818 822 widget = self.active_frontend
819 823 if widget.can_cut():
820 824 widget.cut()
821 825
822 826 def copy_active_frontend(self):
823 827 widget = self.active_frontend
824 828 widget.copy()
825 829
826 830 def copy_raw_active_frontend(self):
827 831 self.active_frontend._copy_raw_action.trigger()
828 832
829 833 def paste_active_frontend(self):
830 834 widget = self.active_frontend
831 835 if widget.can_paste():
832 836 widget.paste()
833 837
834 838 def undo_active_frontend(self):
835 839 self.active_frontend.undo()
836 840
837 841 def redo_active_frontend(self):
838 842 self.active_frontend.redo()
839 843
840 844 def reset_magic_active_frontend(self):
841 845 self.active_frontend.execute("%reset")
842 846
843 847 def history_magic_active_frontend(self):
844 848 self.active_frontend.execute("%history")
845 849
846 850 def save_magic_active_frontend(self):
847 851 self.active_frontend.save_magic()
848 852
849 853 def clear_magic_active_frontend(self):
850 854 self.active_frontend.execute("%clear")
851 855
852 856 def who_magic_active_frontend(self):
853 857 self.active_frontend.execute("%who")
854 858
855 859 def who_ls_magic_active_frontend(self):
856 860 self.active_frontend.execute("%who_ls")
857 861
858 862 def whos_magic_active_frontend(self):
859 863 self.active_frontend.execute("%whos")
860 864
861 865 def print_action_active_frontend(self):
862 866 self.active_frontend.print_action.trigger()
863 867
864 868 def export_action_active_frontend(self):
865 869 self.active_frontend.export_action.trigger()
866 870
867 871 def select_all_active_frontend(self):
868 872 self.active_frontend.select_all_action.trigger()
869 873
870 874 def increase_font_size_active_frontend(self):
871 875 self.active_frontend.increase_font_size.trigger()
872 876
873 877 def decrease_font_size_active_frontend(self):
874 878 self.active_frontend.decrease_font_size.trigger()
875 879
876 880 def reset_font_size_active_frontend(self):
877 881 self.active_frontend.reset_font_size.trigger()
878 882
879 883 def guiref_active_frontend(self):
880 884 self.active_frontend.execute("%guiref")
881 885
882 886 def intro_active_frontend(self):
883 887 self.active_frontend.execute("?")
884 888
885 889 def quickref_active_frontend(self):
886 890 self.active_frontend.execute("%quickref")
887 891 #---------------------------------------------------------------------------
888 892 # QWidget interface
889 893 #---------------------------------------------------------------------------
890 894
891 895 def closeEvent(self, event):
892 896 """ Forward the close event to every tabs contained by the windows
893 897 """
894 898 if self.tab_widget.count() == 0:
895 899 # no tabs, just close
896 900 event.accept()
897 901 return
898 902 # Do Not loop on the widget count as it change while closing
899 903 title = self.window().windowTitle()
900 904 cancel = QtGui.QMessageBox.Cancel
901 905 okay = QtGui.QMessageBox.Ok
902 906
903 907 if self.confirm_exit:
904 908 if self.tab_widget.count() > 1:
905 909 msg = "Close all tabs, stop all kernels, and Quit?"
906 910 else:
907 911 msg = "Close console, stop kernel, and Quit?"
908 912 info = "Kernels not started here (e.g. notebooks) will be left alone."
909 913 closeall = QtGui.QPushButton("&Quit", self)
910 914 closeall.setShortcut('Q')
911 915 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
912 916 title, msg)
913 917 box.setInformativeText(info)
914 918 box.addButton(cancel)
915 919 box.addButton(closeall, QtGui.QMessageBox.YesRole)
916 920 box.setDefaultButton(closeall)
917 921 box.setEscapeButton(cancel)
918 922 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
919 923 box.setIconPixmap(pixmap)
920 924 reply = box.exec_()
921 925 else:
922 926 reply = okay
923 927
924 928 if reply == cancel:
925 929 event.ignore()
926 930 return
927 931 if reply == okay:
928 932 while self.tab_widget.count() >= 1:
929 933 # prevent further confirmations:
930 934 widget = self.active_frontend
931 935 widget._confirm_exit = False
932 936 self.close_tab(widget)
933 937 event.accept()
934 938
General Comments 0
You need to be logged in to leave comments. Login now