Merge pull request #506 from andrewcsmith/patch-2
[supercollider.git] / editors / sced / sced3 / supercollider.py
blob8ab851ab41f2bba229e489e05e55a3318ba7152d
1 # -*- coding: utf-8 -*-
2 # Copyright 2009-2011 Artem Popov and contributors (see AUTHORS)
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 import re, subprocess, time
18 from gi.repository import GObject, Gedit, Gtk, Pango
20 ui_str = """<ui>
21 <menubar name="MenuBar">
22 <menu name="ToolsMenu" action="Tools">
23 <placeholder name="ToolsOps_5">
24 <menuitem action="ScedSuperColliderMode"/>
25 </placeholder>
26 </menu>
27 </menubar>
28 </ui>"""
30 scui_str = """<ui>
31 <menubar name="MenuBar">
32 <placeholder name="ExtraMenu_1">
33 <menu action="SuperColliderMenu">
34 <menuitem action="ScedEvaluate"/>
35 <separator/>
36 <menuitem action="ScedStopSound"/>
37 <menuitem action="ScedRecord"/>
38 <separator/>
39 <menuitem action="ScedServerGUI"/>
40 <menuitem action="ScedStartServer"/>
41 <menuitem action="ScedStopServer"/>
42 <separator/>
43 <menuitem action="ScedFindHelp"/>
44 <menuitem action="ScedBrowseHelp"/>
45 <menuitem action="ScedSearchHelp"/>
46 <menuitem action="ScedMethodArgs"/>
47 <separator/>
48 <menuitem action="ScedFindDefinition"/>
49 <menuitem action="ScedBrowseClass"/>
50 <menuitem action="ScedInspectObject"/>
51 <menuitem action="ScedOpenDevFile"/>
52 <separator/>
53 <menuitem action="ScedFrontWindows"/>
54 <separator/>
55 <menuitem action="ScedRestartInterpreter"/>
56 <menuitem action="ScedRecompile"/>
57 <menuitem action="ScedClearOutput"/>
58 </menu>
59 </placeholder>
60 </menubar>
62 <toolbar name="ToolBar">
63 <separator/>
64 <toolitem action="ScedRecord"/>
65 </toolbar>
66 </ui>"""
68 def is_block_beginning(s):
69 s = "".join(s.split())
70 if s == "(" or s.startswith("(//") or s.startswith("(/*"):
71 return True
72 else:
73 return False
75 def find_block(doc, where=None):
76 if where is None:
77 i1 = doc.get_iter_at_mark(doc.get_insert())
78 else:
79 i1 = where.copy()
81 # move backward until a block beginning is found
82 while True:
83 i1.set_line_offset(0)
85 i2 = i1.copy()
86 i2.forward_to_line_end()
88 if is_block_beginning(doc.get_text(i1, i2, False)):
89 break
91 if not i1.backward_line():
92 raise RuntimeError("Couldn't find where code block starts!")
94 i2 = i1.copy()
95 count = 1
97 line_comment = False
98 block_comment = 0
100 # move forward to the end of the block
101 while True:
102 if not i2.forward_char():
103 raise RuntimeError("Couldn't find where code block ends!")
105 char = i2.get_char()
107 i3 = i2.copy()
108 i3.forward_chars(2)
109 ct = i2.get_text(i3)
111 if ct == "*/":
112 block_comment -= 1
113 elif ct == "/*":
114 block_comment += 1
115 elif ct == "//":
116 line_comment = True
117 elif char == "\n" and line_comment:
118 line_comment = False
120 if not block_comment and not line_comment:
121 if char == "(":
122 count += 1
123 elif char == ")":
124 count -= 1
126 if count == 0:
127 break
129 # include 2 more characters just in case "where" is near the end
130 i2.forward_chars(2)
132 if where.in_range(i1, i2):
133 return i1, i2
134 else:
135 raise RuntimeError("Couldn't find code block!")
137 def class_char_predicate(c, *args):
138 if re.match("[A-Za-z0-9_]", c):
139 return False
140 return True
142 def find_word(doc, where=None):
143 if where is None:
144 i1 = doc.get_iter_at_mark(doc.get_insert())
145 else:
146 i1 = where.copy()
148 #scclass_regex = "[A-Za-z0-9_]"
150 while i1.backward_char():
151 if not re.match("[A-Za-z0-9_]", i1.get_char()):
152 break
154 if not i1.is_start():
155 i1.forward_char()
156 i2 = i1.copy()
158 while i2.forward_char():
159 if not re.match("[A-Za-z0-9_]", i2.get_char()):
160 break
162 # FIXME: find_char no longer works with gir bindings
163 #i1.backward_find_char(class_char_predicate, None, None)
164 #if not i1.is_start():
165 # i1.forward_char()
166 #i2 = i1.copy()
167 #i2.forward_find_char(class_char_predicate, None, None)
169 return i1, i2
171 class ScLang:
172 def __init__(self):
173 self.__sclang = None
175 def start (self):
176 if self.running():
177 return
179 # FIXME: maybe we need a default value in Settings?
180 #folder = self.__settings.props.runtime_folder
181 #if folder is None:
182 # folder = os.getcwd()
184 self.__sclang = subprocess.Popen(["sclang",
185 "-i", "sced"],#, "-d", folder],
186 bufsize=0,
187 stdin=subprocess.PIPE,
188 stdout=subprocess.PIPE,
189 stderr=subprocess.STDOUT,
190 close_fds=True)
191 self.stdout = self.__sclang.stdout
192 self.stdin = self.__sclang.stdin
194 def stop(self):
195 if self.running():
196 self.stdin.close()
197 self.__sclang.wait()
198 self.__sclang = None
200 def running(self):
201 return (self.__sclang is not None) and (self.__sclang.poll() is None)
203 # FIXME: use sclang.communicate()
204 def evaluate(self, code, silent=False):
205 self.stdin.write(bytes(code))
206 if silent:
207 self.stdin.write(bytes("\x1b"))
208 else:
209 self.stdin.write(bytes("\x0c"))
210 self.stdin.flush()
212 def toggle_recording(self, record):
213 if record:
214 self.evaluate("s.prepareForRecord;", silent=True)
215 time.sleep(0.1) # give server some time to prepare
216 self.evaluate("s.record;", silent=True)
217 else:
218 self.evaluate("s.stopRecording;", silent=True)
220 def stop_sound(self):
221 self.evaluate("thisProcess.stop;", silent=True)
223 class Logger:
224 def __init__(self, pipe, log_view):
225 self.__log_view = log_view
227 tag_table = log_view.buffer.get_tag_table()
228 self.__tag = Gtk.TextTag()
230 self.__good_tag = GObject.new(Gtk.TextTag,
231 weight = Pango.Weight.BOLD,
232 foreground = "darkgreen",
233 paragraph_background = "lightgreen")
235 self.__bad_tag = GObject.new(Gtk.TextTag,
236 weight = Pango.Weight.BOLD,
237 foreground = "darkred",
238 paragraph_background = "pink")
240 # for warnings, etc.
241 self.__ugly_tag = GObject.new(Gtk.TextTag,
242 #weight = pango.WEIGHT_BOLD,
243 foreground = "red")
245 tag_table.add(self.__tag)
246 tag_table.add(self.__good_tag)
247 tag_table.add(self.__bad_tag)
248 tag_table.add(self.__ugly_tag)
250 self.__watch_id = GObject.io_add_watch(pipe,
251 GObject.IO_IN |
252 GObject.IO_PRI |
253 GObject.IO_ERR |
254 GObject.IO_HUP,
255 self.__on_output)
257 def __on_output(self, source, condition):
258 s = source.readline()
259 if s == '':
260 self.__append_to_buffer("EOF")
261 return False
263 # FIXME: A workaround for a mac character
264 self.__append_to_buffer(bytes(s))
266 if condition & GObject.IO_ERR:
267 s = source.read() # can safely read until EOF here
268 self.__append_to_buffer(bytes(s))
269 return False
270 elif condition & GObject.IO_HUP:
271 s = source.read() # can safely read until EOF here
272 self.__append_to_buffer(bytes(s))
273 return False
274 elif condition != 1:
275 return False
276 return True
278 def __append_to_buffer(self, text):
279 buffer = self.__log_view.buffer
281 if text.startswith("ERROR"):
282 tags = self.__bad_tag
284 elif text.startswith("WARNING") or text.startswith("FAILURE"):
285 tags = self.__ugly_tag
287 elif text.startswith("Welcome to SuperCollider"):
288 tags = self.__good_tag
289 else:
290 tags = self.__tag
292 buffer.insert_with_tags(buffer.get_end_iter(), text.rstrip(), tags)
293 buffer.insert(buffer.get_end_iter(), "\n")
295 buffer.place_cursor(buffer.get_end_iter())
296 self.__log_view.view.scroll_mark_onscreen(buffer.get_insert())
298 # only required for thread-based implementation
299 # return False
301 def stop(self):
302 GObject.source_remove(self.__watch_id)
304 class LogPanel(Gtk.ScrolledWindow):
305 def __init__(self):
306 Gtk.ScrolledWindow.__init__(self)
308 self.props.shadow_type = Gtk.ShadowType.IN
309 self.props.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC
310 self.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC
312 self.buffer = Gtk.TextBuffer()
313 self.view = Gtk.TextView()
314 self.view.modify_font(Pango.FontDescription("Monospace"))
315 self.view.props.buffer = self.buffer
316 self.view.props.editable = False
317 self.view.props.wrap_mode = Gtk.WrapMode.CHAR
319 self.add(self.view)
320 self.view.show()
322 class ScedWindowActivatable(GObject.Object, Gedit.WindowActivatable):
323 __gtype_name__ = "ScedWindowActivatable"
324 window = GObject.property(type=Gedit.Window)
326 def __init__(self):
327 GObject.Object.__init__(self)
329 def do_activate(self):
330 self.__insert_menu()
332 def do_deactivate(self):
333 self.__remove_menu()
335 def do_update_state(self):
336 pass
338 def __remove_menu(self):
339 manager = self.window.get_ui_manager()
340 manager.remove_ui(self.__ui_id)
341 manager.remove_action_group(self.__actions)
342 manager.ensure_update()
344 def __insert_menu(self):
345 manager = self.window.get_ui_manager()
346 self.__actions = Gtk.ActionGroup("ScedActions")
348 toggle_entries = [
349 ("ScedSuperColliderMode", None, "_SuperCollider Mode", None,
350 _("Toggle SuperCollider interaction mode"),
351 self.on_sc_mode_activate,
352 False)
355 self.__actions.add_toggle_actions(toggle_entries)
357 manager.insert_action_group(self.__actions, -1)
358 self.__ui_id = manager.add_ui_from_string(ui_str)
359 manager.ensure_update()
361 def __remove_sc_menu(self):
362 manager = self.window.get_ui_manager()
363 manager.remove_ui(self.__scui_id)
364 manager.remove_action_group(self.__sc_actions)
365 manager.ensure_update()
367 def __insert_sc_menu(self):
368 manager = self.window.get_ui_manager()
369 self.__sc_actions = Gtk.ActionGroup("SuperColliderActions")
371 entries = [
372 ("SuperColliderMenu", None, "Super_Collider"),
374 ("ScedEvaluate", Gtk.STOCK_EXECUTE, _("Evaluate"), "<control>E",
375 _("Evaluate line or selection"),
376 self.on_evaluate),
378 ("ScedStopSound", Gtk.STOCK_STOP, _("Stop Sound"), "Escape",
379 _("Stop sound and free all server nodes"),
380 self.on_stop_sound),
382 ("ScedFindHelp", None, _("Find Help"), "<control>U",
383 _("Find and open help file"),
384 self.on_find_help),
386 ("ScedBrowseHelp", None, _("Browse Help"), "<control><alt>U",
387 _("Browse help by categories"),
388 self.on_browse_help),
390 ("ScedSearchHelp", None, _("Search Help"), None,
391 _("Search for help"),
392 self.on_search_help),
394 ("ScedMethodArgs", None, _("Show method args"), "<alt>A",
395 _("Show method arguments and defaults"),
396 self.on_method_args),
398 ("ScedFindDefinition", None, _("Find Definition"), "<control>Y",
399 _("Find and open class definition"),
400 self.on_find_definition),
402 ("ScedBrowseClass", None, _("Browse class"), None,
403 _("Browse class (needs running SwingOSC server)"),
404 self.on_browse_class),
406 ("ScedInspectObject", None, _("Inspect Object"), None,
407 _("Inspect object state (needs running SwingOSC server)"),
408 self.on_inspect_object),
410 ("ScedOpenDevFile", None, _("Open development file"), "<control><alt>K",
411 _("Open corresponding development file for current document"),
412 self.on_open_dev_file),
414 ("ScedRestartInterpreter", None, _("Restart Interpreter"), None,
415 _("Restart sclang"),
416 self.on_restart),
418 ("ScedRecompile", None, _("Recompile class library"), "<control><shift>R",
419 _("Recompile class library"),
420 self.on_recompile),
422 ("ScedClearOutput", Gtk.STOCK_CLEAR, _("Clear output"), None,
423 _("Clear interpreter log"),
424 self.on_clear_log),
426 ("ScedStartServer", None, _("Start Server"), None,
427 _("Start the default server"),
428 self.on_start_server),
430 ("ScedStopServer", None, _("Stop Server"), None,
431 _("Stop the default server"),
432 self.on_stop_server),
434 ("ScedServerGUI", None, _("Show Server GUI"), None,
435 _("Show GUI for default server"),
436 self.on_server_gui),
438 ("ScedFrontWindows", None, _("Raise all windows"), "<alt>W",
439 _("Raise all windows"),
440 self.on_front_windows),
443 toggle_entries = [
444 ("ScedRecord", Gtk.STOCK_MEDIA_RECORD, "Record", None,
445 _("Toggle recording"),
446 self.on_record,
447 False)
450 self.__sc_actions.add_actions(entries)
451 self.__sc_actions.add_toggle_actions(toggle_entries)
452 manager.insert_action_group(self.__sc_actions, -1)
453 self.__scui_id = manager.add_ui_from_string(scui_str)
454 manager.ensure_update()
456 def on_sc_mode_activate(self, action, data=None):
457 if action.get_active():
458 self.__log_panel = LogPanel()
459 panel = self.window.get_bottom_panel()
460 panel.show()
461 panel.add_item_with_stock_icon(self.__log_panel,
462 "SuperCollider", _("SuperCollider output"), Gtk.STOCK_EXECUTE)
463 self.__log_panel.show()
465 self.__lang = ScLang()
466 self.__lang.start()
468 self.__logger = Logger(self.__lang.stdout, self.__log_panel)
469 self.__insert_sc_menu()
470 else:
471 panel = self.window.get_bottom_panel()
472 panel.remove_item(self.__log_panel)
473 # FIXME: un-record
474 self.__lang.stop()
475 self.__logger.stop()
476 self.__remove_sc_menu()
478 def on_evaluate(self, action, data=None):
479 doc = self.window.get_active_document()
481 try:
482 i1, i2 = doc.get_selection_bounds()
483 except ValueError:
484 i1 = doc.get_iter_at_mark(doc.get_insert())
485 i1.set_line_offset(0)
486 i2 = i1.copy()
487 i2.forward_to_line_end()
489 if is_block_beginning(doc.get_text(i1, i2, False)):
490 try:
491 i1, i2 = find_block(doc, i1)
492 except RuntimeError:
493 #statusbar = self.window.get_statusbar()
494 #context = statusbar.get_context_id("supercollider")
495 # FIXME: no longer works with gir
496 #statusbar.flash_message(context,
497 # "Code block is not properly closed")
498 return
499 doc.select_range(i1, i2)
501 text = doc.get_text(i1, i2, False)
502 self.__lang.evaluate(text)
504 def on_stop_sound(self, action, data=None):
505 record = self.__sc_actions.get_action("ScedRecord");
506 if record.get_active():
507 record.activate() # untoggle
508 self.__lang.stop_sound()
510 def on_record(self, action, data=None):
511 self.__lang.toggle_recording(action.get_active())
513 def get_selection(self):
514 doc = self.window.get_active_document()
516 try:
517 i1, i2 = doc.get_selection_bounds()
518 except ValueError:
519 i1 = doc.get_iter_at_mark(doc.get_insert())
520 i1, i2 = find_word(doc, i1)
521 doc.select_range(i1, i2)
523 return doc.get_text(i1, i2, False)
525 def on_find_help(self, action, data=None):
526 text = self.get_selection()
527 cmd = 'HelpBrowser.openHelpFor(\"' + text + '\");'
528 self.__lang.evaluate(cmd, silent=True)
530 def on_browse_help(self, action):
531 self.__lang.evaluate("HelpBrowser.openBrowsePage;")
533 def on_search_help(self, action):
534 text = self.get_selection()
535 self.__lang.evaluate("HelpBrowser.openSearchPage(\"" + text + "\");")
537 def on_method_args(self, action):
538 text = self.get_selection()
539 self.__lang.evaluate("Help.methodArgs(\"" + text + "\");")
541 def on_find_definition(self, action, data=None):
542 text = self.get_selection()
543 self.__lang.evaluate("" + text + ".openCodeFile", silent=True)
545 def on_browse_class(self, action):
546 text = self.get_selection()
547 self.__lang.evaluate("" + text + ".browse", silent=True)
549 def on_inspect_object(self, action, data=None):
550 text = self.get_selection()
551 self.__lang.evaluate("" + text + ".inspect", silent=True)
553 def on_open_dev_file(self, action):
554 doc = self.window.get_active_document()
555 if doc is None:
556 return None
557 location = doc.get_location()
558 if location is not None and Gedit.utils_location_has_file_scheme(location):
559 self.__lang.evaluate("thisProcess.platform.devLoc(\""+location.get_path()+"\").openTextFile", silent=True)
561 def on_recompile(self, action):
562 self.__lang.stdin.write(bytes("\x18"))
564 def on_restart(self, action, data=None):
565 if self.__lang.running():
566 self.__lang.stop()
567 self.__lang.start()
568 self.__logger = Logger(self.__lang.stdout, self.__log_panel)
570 def on_clear_log(self, action, data=None):
571 self.__log_panel.buffer.set_text("")
573 def on_start_server(self, action, data=None):
574 # FIXME: make these actions possible only if interpreter is running and okay
575 self.__lang.evaluate("Server.default.boot;", silent=True)
577 def on_stop_server(self, action, data=None):
578 # FIXME: make these actions possible only if interpreter is running and okay
579 self.__lang.evaluate("Server.default.quit;", silent=True)
581 def on_server_gui(self, action, data=None):
582 self.__lang.evaluate("Server.default.makeGui;", silent=True)
584 def on_front_windows(self, action, data=None):
585 self.__lang.evaluate("Window.allWindows.do(_.front);", silent=True)