Ticket #4949: lilypond.py

File lilypond.py, 8.7 KB (added by Link Mauve <linkmauve@…>, 7 years ago)

The plugin.

Line 
1# -*- coding: utf-8 -*-
2## This file is part of Gajim.
3##
4## Gajim is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published
6## by the Free Software Foundation; version 3 only.
7##
8## Gajim is distributed in the hope that it will be useful,
9## but WITHOUT ANY WARRANTY; without even the implied warranty of
10## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11## GNU General Public License for more details.
12##
13## You should have received a copy of the GNU General Public License
14## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
15##
16
17'''
18LilyPond Plugin.
19
20based on LaTeX Plugin by Yves Fischer
21:author: Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
22:since: 4th April 2009
23:copyright: Copyright (2009) Emmanuel Gil Peyrot
24:license: GPL
25'''
26
27
28from threading import Thread
29import os,gtk,gobject,tempfile,random,subprocess
30
31from plugins import GajimPlugin
32from plugins.helpers import log, log_calls
33
34
35gtk.gdk.threads_init() ##for gtk.gdk.thread_[enter|leave]()
36
37
38#following constants are taken from Pidgin Lilypond Plugin
39def lilypond_template(code):
40        return """\\include "italiano.ly"
41\\header {tagline=""}
42\\relative do\' {
43%s
44\\bar "|."
45}""" % (code)
46
47
48
49"""
50
51"""
52class LilypondRenderer(Thread):
53        def __init__(self, iter_start, iter_end, buffer, widget):
54                Thread.__init__(self)
55
56                self.code = iter_start.get_text(iter_end)
57                self.mark_name = "LilypondRendererMark%s" % (random.randint(0,1000).__str__())
58                self.mark = buffer.create_mark(self.mark_name, iter_start, True)
59
60                self.buffer = buffer
61                self.widget = widget
62
63                #delete code and show message "processing"
64                self.buffer.delete(iter_start, iter_end)
65                #iter_start.forward_char()
66                self.buffer.insert(iter_start, "Processing LilyPond")
67
68                self.start() #start background processing
69
70        def run(self):
71#               gtk.gdk.threads_enter() #its nearly non-sense using this in a full thread
72                #should be fine-grained later
73                try:
74                        self.show_image()
75                except:
76                        pass
77                finally:
78        #               gtk.gdk.threads_leave()
79                        self.buffer.delete_mark(self.mark)
80
81        """
82        String -> TextBuffer
83        """
84        def show_error(self, message):
85                gtk.gdk.threads_enter()
86                iter_mark = self.buffer.get_iter_at_mark(self.mark)
87                iter_end = iter_mark.copy().forward_search("Processing LilyPond", gtk.TEXT_SEARCH_TEXT_ONLY)[1]
88                self.buffer.delete(iter_mark, iter_end)
89
90
91                pixbuf = self.widget.render_icon(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON)
92                self.buffer.insert_pixbuf(iter_end, pixbuf)
93                self.buffer.insert(iter_end, message)
94                gtk.gdk.threads_leave()
95
96        """
97        Lilypond -> PNG -> TextBuffer
98        """
99        @log_calls("LilypondRenderer.show_image()")
100        def show_image(self):
101                fn = "gajim-lilypond-" + random.randint(0,1000).__str__()
102                log.debug("Generate %s.ps" % os.path.join(tempfile.gettempdir(), fn))
103                try:
104                        p_lilypond = subprocess.Popen(("lilypond -f ps %s.ly" % (fn)).split(" "), stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=tempfile.gettempdir())
105                        log.debug("lilypond process spawned, write lilypond code to sdtin")
106                        log.debug(lilypond_template(self.code[2:len(self.code)-2]))
107                        p_lilypond.stdin.write(lilypond_template(self.code[2:len(self.code)-2]))
108                        p_lilypond.stdin.close()
109                        log.debug("Wait for lilypond-process to finish")
110                        p_lilypond.wait()
111                        if p_lilypond.returncode != 0:
112                                err = p_lilypond.stdout.read()
113                                raise OSError("Error in lilypond code: " + err)
114                except OSError, e:
115                        self.show_error("lilypond error: " + e.message + "\n" + "===ORIGINAL CODE====\n" + self.code[2:len(self.code)-1])
116                        log.debug("lilypond error: " + e.message)
117                        log.debug("lilypond code: " + self.code[2:len(self.code)-2])
118                        return False
119
120                log.debug("PS OK")
121                log.debug("Generate %s.png from PS using convert" % os.path.join(tempfile.gettempdir(), fn))
122                try:
123                        p_convert = subprocess.Popen(("convert -trim %s.ps %s.png" % (fn, fn)).split(" "),stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,cwd=tempfile.gettempdir())
124                        p_convert.stdin.close() #i have nothing to say
125                        log.debug("wait for convert-processs to finish")
126                        p_convert.wait()
127                        if p_convert.returncode != 0:
128                                err = p_convert.stdout.read() + p_convert.stderr.read()
129                                raise OSError("Error in convert ps->png conversion subprocess: " + err)
130                except OSError, e:
131                        self.show_error("Can't execute convert: " + e.message)
132                        log.debug("Can't execute convert: " + e.message)
133                        return False
134                finally:
135                        os.remove(os.path.join(tempfile.gettempdir(), fn + ".ps"))
136                log.debug("PNG OK")
137                log.debug("Loading PNG %s" % os.path.join(tempfile.gettempdir(),fn+".png"))
138                try:
139                        gtk.gdk.threads_enter()
140                        pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(tempfile.gettempdir(),fn+".png"))
141                        log.debug("png loaded")
142                        iter_mark = self.buffer.get_iter_at_mark(self.mark)
143                        iter_end = iter_mark.copy().forward_search("Processing LilyPond", gtk.TEXT_SEARCH_TEXT_ONLY)[1]
144                        log.debug("Delete old Text")
145                        self.buffer.delete(iter_mark, iter_end)
146                        log.debug("Insert pixbuf")
147                        self.buffer.insert_pixbuf(iter_end, pixbuf)
148                        log.debug("OK _ DONE")
149                except gobject.GError:
150                        self.show_error("Cant open %s.png for reading" % os.path.join(tempfile.gettempdir(),fn))
151                        log.debug("Cant open %s.png for reading" % os.path.join(tempfile.gettempdir(),fn))
152                finally:
153                        gtk.gdk.threads_leave()
154                        os.remove(os.path.join(tempfile.gettempdir(), fn + ".png"))
155
156
157
158class LilypondPluginConfiguration(gtk.Window):
159        def __init__(self):
160                gtk.Window.__init__(self)
161                self.set_title("Lilypond Plugin Configuration")
162
163                self.pane = gtk.VBox()
164                self.add(self.pane)
165
166                self.btn_test = gtk.Button("Test Lilypond Configuration")
167                self.btn_test.connect("clicked", self.start_test)
168                self.pane.pack_start(self.btn_test,expand=False)
169
170                self.lbl_messages = gtk.Label("Results:\n")
171                self.lbl_messages.set_line_wrap(True)
172                self.pane.pack_start(self.lbl_messages)
173
174        def log(self, message):
175                self.lbl_messages.set_text(self.lbl_messages.get_text() + "\n" + message)
176
177
178        """
179        performs very simple checks (check if executable is in PATH)
180        """
181        def start_test(self,widget):
182                log = self.log
183                from os import system
184                log("Start Test")
185                log("Test Lilypond Binary")
186                ret = system("lilypond -version")
187                if ret != 0:
188                        log("No LilyPond binary found in PATH")
189                else:
190                        log("OK")
191                log("Test convert")
192                ret = system("convert -version")
193                if ret != 0:
194                        log("No convert binary found in PATH")
195                else:
196                        log("OK")
197
198        def run(self,parent):
199                self.present()
200                self.show_all()
201
202
203
204
205
206class LilypondPlugin(GajimPlugin):
207        name = u'Lilypond Plugin'
208        short_name = u'lilypond'
209        version = u'0.1'
210        description = u'''Invoke Lilypond to render %%foobar%% sourrounded Lilypond equations. Needs lilypond and convert'''
211        authors = [u'Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>']
212        homepage = u'http://www.linkmauve.fr/dev/'
213
214
215        def init(self):
216                self.config_dialog = LilypondPluginConfiguration()
217
218                self.gui_extension_points = {
219                        'chat_control_base' : (self.connect_with_chat_control_base,
220                                                                   self.disconnect_from_chat_control_base)
221                }
222
223        """
224        start rendering if clicked on a link
225        """
226        def textview_event_after(self, tag, widget, event, iter):
227                if tag.get_property("name") != "lilypond" or event.type != gtk.gdk.BUTTON_PRESS:
228                        return
229                dollar_start, iter_start = iter.backward_search("%%", gtk.TEXT_SEARCH_TEXT_ONLY)
230                iter_end, dollar_end = iter.forward_search("%%", gtk.TEXT_SEARCH_TEXT_ONLY)
231                LilypondRenderer(dollar_start, dollar_end, widget.get_buffer(), widget)
232
233        """
234        called when conversation text widget changes
235        """
236        def textbuffer_live_lilypond_expander(self, tb):
237                def split_list(list):
238                        newlist = []
239                        for i in range(0,len(list)-1,2):
240                                newlist.append( [ list[i], list[i+1], ] )
241                        return newlist
242
243                assert isinstance(tb,gtk.TextBuffer)
244                start_iter = tb.get_start_iter()
245                points = []
246                tuple_found = start_iter.forward_search("%%", gtk.TEXT_SEARCH_TEXT_ONLY)
247                while tuple_found != None:
248                        points.append(tuple_found)
249                        tuple_found = tuple_found[1].forward_search("%%", gtk.TEXT_SEARCH_TEXT_ONLY)
250
251                for pair in split_list(points):
252                        tb.apply_tag_by_name("lilypond", pair[0][1], pair[1][0])
253
254        def connect_with_chat_control_base(self, chat_control):
255                d = {}
256                tv = chat_control.conv_textview.tv
257                tb = tv.get_buffer()
258
259                self.lilypond_tag = gtk.TextTag("lilypond")
260                self.lilypond_tag.set_property('foreground', "blue")
261                self.lilypond_tag.set_property('underline', "single")
262                d['tag_id'] = self.lilypond_tag.connect('event', self.textview_event_after)
263                tb.get_tag_table().add(self.lilypond_tag)
264
265                d['h_id'] = tb.connect('changed', self.textbuffer_live_lilypond_expander)
266                chat_control.lilyponds_expander_plugin_data = d
267
268                return True
269
270        def disconnect_from_chat_control_base(self, chat_control):
271                d = chat_control.lilyponds_expander_plugin_data
272                #tv = chat_control.msg_textview
273                tv = chat_control.conv_textview.tv
274
275                tv.get_buffer().disconnect(d['h_id'])
276                self.lilypond_tag.disconnect(d['tag_id'])
277