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