~ubuntu-branches/ubuntu/saucy/solfege/saucy

« back to all changes in this revision

Viewing changes to solfege/fpeditor.py

  • Committer: Bazaar Package Importer
  • Author(s): Tom Cato Amundsen
  • Date: 2010-03-28 06:34:28 UTC
  • mfrom: (1.1.10 upstream) (2.1.7 sid)
  • Revision ID: james.westby@ubuntu.com-20100328063428-wg2bqvoce2aq4xfb
Tags: 3.15.9-1
* New upstream release.
* Redo packaging. 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: set fileencoding=utf-8 :
 
2
# GNU Solfege - free ear training software
 
3
# Copyright (C) 2009  Tom Cato Amundsen
 
4
#
 
5
# This program is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation, either version 3 of the License, or
 
8
# (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
17
 
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
import logging
 
22
import os
 
23
import StringIO
 
24
import subprocess
 
25
import sys
 
26
import time
 
27
import weakref
 
28
 
 
29
import gobject
 
30
import gtk
 
31
 
 
32
if __name__ == '__main__':
 
33
    from solfege import i18n
 
34
    i18n.setup(".", "C")
 
35
    import solfege.statistics
 
36
    solfege.db = solfege.statistics.DB()
 
37
 
 
38
import solfege
 
39
from solfege import cfg
 
40
from solfege import filesystem
 
41
from solfege import gu
 
42
from solfege import frontpage as pd
 
43
from solfege import lessonfile
 
44
from solfege import lessonfilegui
 
45
from solfege import osutils
 
46
 
 
47
class LessonFilePreviewWidget(gtk.VBox):
 
48
    def __init__(self, model):
 
49
        gtk.VBox.__init__(self)
 
50
        self.m_model = model
 
51
        self.set_size_request(200, 200)
 
52
        l = gtk.Label()
 
53
        l.set_alignment(0.0, 0.5)
 
54
        l.set_markup("<b>Title:</b>")
 
55
        self.pack_start(l, False)
 
56
        self.g_title = gtk.Label()
 
57
        self.g_title.set_alignment(0.0, 0.5)
 
58
        self.pack_start(self.g_title, False)
 
59
        l = gtk.Label()
 
60
        l.set_alignment(0.0, 0.5)
 
61
        l.set_markup("<b>Module:</b>")
 
62
        self.pack_start(l, False)
 
63
        self.g_module = gtk.Label()
 
64
        self.g_module.set_alignment(0.0, 0.5)
 
65
        self.pack_start(self.g_module, False)
 
66
        l = gtk.Label()
 
67
        l.set_alignment(0.0, 0.5)
 
68
        l.set_markup("<b>Used in topcis:</b>")
 
69
        self.pack_start(l, False)
 
70
        self.g_topic_box = gtk.VBox()
 
71
        self.pack_start(self.g_topic_box, False)
 
72
        self.show_all()
 
73
    def update(self, dlg):
 
74
        fn = dlg.get_preview_filename()
 
75
        if fn:
 
76
            fn = gu.decode_filename(fn)
 
77
            for child in self.g_topic_box.get_children():
 
78
                child.destroy()
 
79
            fn = lessonfile.mk_uri(fn)
 
80
            try:
 
81
                self.set_sensitive(True)
 
82
                self.g_title.set_text(lessonfile.infocache.get(fn, 'title'))
 
83
                self.g_module.set_text(lessonfile.infocache.get(fn, 'module'))
 
84
                self.g_ok_button.set_sensitive(True)
 
85
                for x in self.m_model.iterate_topics_for_file(fn):
 
86
                    l = gtk.Label(x)
 
87
                    l.set_alignment(0.0, 0.5)
 
88
                    self.g_topic_box.pack_start(l, False)
 
89
                if not self.g_topic_box.get_children():
 
90
                    l = gtk.Label(u"-")
 
91
                    l.set_alignment(0.0, 0.5)
 
92
                    self.g_topic_box.pack_start(l, False)
 
93
            except (lessonfile.InfoCache.FileNotFound, 
 
94
                    lessonfile.InfoCache.FileNotLessonfile), e:
 
95
                self.g_title.set_text(u'')
 
96
                self.g_module.set_text(u'')
 
97
                self.g_ok_button.set_sensitive(False)
 
98
                self.set_sensitive(False)
 
99
        self.show_all()
 
100
        return True
 
101
 
 
102
class SelectLessonFileDialog(gtk.FileChooserDialog):
 
103
    def __init__(self, parent):
 
104
        gtk.FileChooserDialog.__init__(self, _("Select lesson file"),
 
105
            parent=parent,
 
106
            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,))
 
107
        self.set_select_multiple(True)
 
108
        pv = LessonFilePreviewWidget(parent.m_model)
 
109
        pv.g_ok_button = self.add_button("gtk-ok", gtk.RESPONSE_OK)
 
110
        pv.g_ok_button.set_sensitive(False)
 
111
        pv.show()
 
112
        self.set_preview_widget(pv)
 
113
        self.connect('selection-changed', pv.update)
 
114
 
 
115
 
 
116
def editor_of(obj):
 
117
    """
 
118
    Return the toplevel page, the one that is a Editor object.
 
119
    """
 
120
    p = obj
 
121
    while not isinstance(p, Editor):
 
122
        p = p.m_parent
 
123
    return p
 
124
 
 
125
def parent_page(obj):
 
126
    """
 
127
    Return the parent page of obj. Return None if this is the toplevel page.
 
128
    """
 
129
    p = obj
 
130
    while True:
 
131
        p = p.m_parent
 
132
        if isinstance(p, Page):
 
133
            return p
 
134
        if p is None:
 
135
            return None
 
136
    
 
137
class Section(gtk.VBox):
 
138
    """
 
139
    A section consists of a heading and a list of links.
 
140
    self.g_link_box is a vbox that contains the links.
 
141
    """
 
142
    def __init__(self, model, parent):
 
143
        gtk.VBox.__init__(self)
 
144
        self.m_model = model
 
145
        self.m_parent = parent
 
146
        assert isinstance(model, pd.LinkList)
 
147
        hbox = gtk.HBox()
 
148
        hbox.set_spacing(6)
 
149
        self.pack_start(hbox, False)
 
150
        # This is displayed and used when we edit the heading
 
151
        self.g_heading_entry = gtk.Entry()
 
152
        self.g_heading_entry.set_no_show_all(True)
 
153
        hbox.pack_start(self.g_heading_entry)
 
154
        self.g_heading = gtk.Label()
 
155
        self.g_heading.set_alignment(0.0, 0.5)
 
156
        # FIXME escape m_name
 
157
        self.g_heading.set_markup("<b>%s</b>" % model.m_name)
 
158
        hbox.pack_start(self.g_heading, False)
 
159
        #
 
160
        button_hbox = gtk.HBox()
 
161
        button_hbox.set_spacing(0)
 
162
        hbox.pack_start(button_hbox, False)
 
163
        im = gtk.Image()
 
164
        im.set_from_stock(gtk.STOCK_EDIT, gtk.ICON_SIZE_MENU)
 
165
        button = gtk.Button()
 
166
        button.add(im)
 
167
        button.connect('clicked', self.on_edit_heading)
 
168
        button_hbox.pack_start(button, False)
 
169
        #
 
170
        im = gtk.Image()
 
171
        im.set_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_MENU)
 
172
        button = gtk.Button()
 
173
        button.add(im)
 
174
        button.connect('button-release-event', self.on_add)
 
175
        button_hbox.pack_start(button, False)
 
176
        #
 
177
        im = gtk.Image()
 
178
        im.set_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU)
 
179
        button = gtk.Button()
 
180
        button.add(im)
 
181
        button.connect('button-release-event', self.on_remove)
 
182
        button_hbox.pack_start(button, False)
 
183
        #
 
184
        im = gtk.Image()
 
185
        im.set_from_stock(gtk.STOCK_CUT, gtk.ICON_SIZE_MENU)
 
186
        b = gtk.Button()
 
187
        b.add(im)
 
188
        b.connect('clicked', self.on_cut)
 
189
        button_hbox.pack_start(b, False)
 
190
        #
 
191
        im = gtk.Image()
 
192
        im.set_from_stock(gtk.STOCK_PASTE, gtk.ICON_SIZE_MENU)
 
193
        b = gtk.Button()
 
194
        b.add(im)
 
195
        b.connect('clicked', self.on_paste, -1)
 
196
        Editor.clipboard.register_paste_button(b, (pd.LinkList, pd.Page, unicode))
 
197
        button_hbox.pack_start(b, False)
 
198
        #
 
199
        im = gtk.Image()
 
200
        im.set_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU)
 
201
        self.g_move_down_btn = gtk.Button()
 
202
        self.g_move_down_btn.add(im)
 
203
        self.g_move_down_btn.connect('clicked', self.on_move_down)
 
204
        button_hbox.pack_start(self.g_move_down_btn, False)
 
205
        #
 
206
        im = gtk.Image()
 
207
        im.set_from_stock(gtk.STOCK_GO_UP, gtk.ICON_SIZE_MENU)
 
208
        self.g_move_up_btn = gtk.Button()
 
209
        self.g_move_up_btn.add(im)
 
210
        self.g_move_up_btn.connect('clicked', self.on_move_up)
 
211
        button_hbox.pack_start(self.g_move_up_btn, False)
 
212
        #
 
213
        im = gtk.Image()
 
214
        im.set_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_MENU)
 
215
        self.g_move_left_btn = gtk.Button()
 
216
        self.g_move_left_btn.add(im)
 
217
        self.g_move_left_btn.connect('clicked',
 
218
            parent.m_parent.on_move_section_left, self)
 
219
        button_hbox.pack_start(self.g_move_left_btn, False)
 
220
        #
 
221
        im = gtk.Image()
 
222
        im.set_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_MENU)
 
223
        self.g_move_right_btn = gtk.Button()
 
224
        self.g_move_right_btn.add(im)
 
225
        self.g_move_right_btn.connect('clicked',
 
226
            parent.m_parent.on_move_section_right, self)
 
227
        button_hbox.pack_start(self.g_move_right_btn, False)
 
228
        #
 
229
        self.g_link_box = gtk.VBox()
 
230
        self.pack_start(self.g_link_box, False)
 
231
        for link in self.m_model:
 
232
            self.g_link_box.pack_start(self.create_linkrow(link))
 
233
        # The button to click to add a new link
 
234
        hbox = gtk.HBox()
 
235
        self.pack_start(hbox)
 
236
    def on_move_down(self, btn):
 
237
        self.m_parent.move_section_down(self)
 
238
    def on_move_up(self, btn):
 
239
        self.m_parent.move_section_up(self)
 
240
    def on_edit_heading(self, btn):
 
241
        self.g_heading_entry.set_text(self.m_model.m_name)
 
242
        self.g_heading_entry.show()
 
243
        self.g_heading.hide()
 
244
        self.g_heading_entry.grab_focus()
 
245
        def finish_edit(entry):
 
246
            self.g_heading_entry.disconnect(sid)
 
247
            self.g_heading_entry.disconnect(keyup_id)
 
248
            self.g_heading_entry.disconnect(keydown_sid)
 
249
            self.m_model.m_name = entry.get_text()
 
250
            self.g_heading.set_markup(u"<b>%s</b>" % entry.get_text())
 
251
            self.g_heading_entry.hide()
 
252
            self.g_heading.show()
 
253
        sid = self.g_heading_entry.connect('activate', finish_edit)
 
254
        def keydown(entry, event):
 
255
            if event.keyval == gtk.keysyms.Tab:
 
256
                finish_edit(entry)
 
257
        keydown_sid = self.g_heading_entry.connect('key-press-event', keydown)
 
258
        def keyup(entry, event):
 
259
            if event.keyval == gtk.keysyms.Escape:
 
260
                self.g_heading_entry.disconnect(sid)
 
261
                self.g_heading_entry.disconnect(keyup_id)
 
262
                self.g_heading_entry.hide()
 
263
                self.g_heading.show()
 
264
                return True
 
265
        keyup_id = self.g_heading_entry.connect('key-release-event', keyup)
 
266
    def on_add(self, btn, event):
 
267
        menu = gtk.Menu()
 
268
        item = gtk.MenuItem(_("Add link to new page"))
 
269
        item.connect('activate', self.on_add_link_to_new_page)
 
270
        menu.append(item)
 
271
        item = gtk.MenuItem(_("Add link to exercise"))
 
272
        item.connect('activate', self.on_add_link)
 
273
        menu.append(item)
 
274
        menu.show_all()
 
275
        menu.popup(None, None, None, event.button, event.time)
 
276
    def on_remove(self, btn, event):
 
277
        self.m_parent.remove_section(self)
 
278
    def on_add_link(self, btn):
 
279
        if editor_of(self).m_filename:
 
280
            open_dir = os.path.split(editor_of(self).m_filename)[0]
 
281
        else:
 
282
            open_dir = filesystem.user_data()
 
283
        dlg = SelectLessonFileDialog(editor_of(self))
 
284
        dlg.set_current_folder(open_dir)
 
285
        while 1:
 
286
            ret = dlg.run()
 
287
            if ret in (gtk.RESPONSE_REJECT, gtk.RESPONSE_DELETE_EVENT, gtk.RESPONSE_CANCEL):
 
288
                break
 
289
            else:
 
290
                assert ret == gtk.RESPONSE_OK
 
291
                for filename in dlg.get_filenames():
 
292
                    fn = gu.decode_filename(filename)
 
293
                    assert os.path.isabs(fn)
 
294
                    # If the file name is a file in a subdirectory below
 
295
                    # lessonfile.exercises_dir in the current working directory,
 
296
                    # then the file is a standard lesson file, and it will be
 
297
                    # converted to a uri scheme with:
 
298
                    fn = lessonfile.mk_uri(fn)
 
299
                    # Small test to check that the file actually is a lesson file.
 
300
                    try:
 
301
                        lessonfile.infocache.get(fn, 'title')
 
302
                    except lessonfile.infocache.FileNotLessonfile:
 
303
                        continue
 
304
                    self.m_model.append(fn)
 
305
                    self.g_link_box.pack_start(self.create_linkrow(fn), False)
 
306
                break
 
307
        dlg.destroy()
 
308
    def on_add_link_to_new_page(self, menuitem):
 
309
        page = pd.Page(_("Untitled%s") % "", [pd.Column()])
 
310
        self.m_model.append(page)
 
311
        self.g_link_box.pack_start(self.create_linkrow(page))
 
312
    def create_linkrow(self, link_this):
 
313
        hbox = gtk.HBox()
 
314
        def ff(btn, page):
 
315
            if id(page) in editor_of(self).m_page_mapping:
 
316
                editor_of(self).show_page_id(id(page))
 
317
            else:
 
318
                if not page[0]:
 
319
                    page[0].append(pd.LinkList(link_this.m_name))
 
320
                p = Page(page, parent_page(self))
 
321
                p.show()
 
322
                editor_of(self).add_page(p)
 
323
        if isinstance(link_this, pd.Page):
 
324
            linkbutton = gu.ClickableLabel(link_this.m_name)
 
325
            linkbutton.connect('clicked', ff, link_this)
 
326
        else:
 
327
            try:
 
328
                linkbutton = gu.ClickableLabel(lessonfile.infocache.get(link_this, 'title'))
 
329
                linkbutton.set_tooltip_text(link_this)
 
330
            except lessonfile.InfoCache.FileNotFound:
 
331
                linkbutton = gu.ClickableLabel(_(u"«%s» was not found") % link_this)
 
332
                linkbutton.make_warning()
 
333
 
 
334
        hbox.pack_start(linkbutton)
 
335
        linkbutton.connect('button-press-event', self.on_right_click_row, link_this)
 
336
        hbox.show_all()
 
337
        return hbox
 
338
    def on_right_click_row(self, button, event, linked):
 
339
        idx = self.m_model.index(linked)
 
340
        if event.button == 3:
 
341
            m = gtk.Menu()
 
342
            item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
 
343
            item.connect('activate', self.on_delete_link, linked)
 
344
            m.append(item)
 
345
            item = gtk.ImageMenuItem(gtk.STOCK_CUT)
 
346
            item.connect('activate', self.on_cut_link, idx)
 
347
            m.append(item)
 
348
            item = gtk.ImageMenuItem(gtk.STOCK_PASTE)
 
349
            item.set_sensitive(bool(Editor.clipboard))
 
350
            item.connect('activate', self.on_paste, idx)
 
351
            m.append(item)
 
352
            item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
 
353
            item.connect('activate', self.on_edit_linktext, linked)
 
354
            item.set_sensitive(bool(not isinstance(linked, basestring)))
 
355
            m.append(item)
 
356
            item = gtk.ImageMenuItem(gtk.STOCK_GO_UP)
 
357
            item.connect('activate', self.on_move_up, idx)
 
358
            item.set_sensitive(bool(idx > 0))
 
359
            m.append(item)
 
360
            item = gtk.ImageMenuItem(gtk.STOCK_GO_DOWN)
 
361
            item.connect('activate', self.on_move_down, idx)
 
362
            item.set_sensitive(bool(idx < len(self.m_model) - 1))
 
363
            m.append(item)
 
364
            item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
 
365
            item.set_sensitive(isinstance(linked, unicode))
 
366
            item.connect('activate', self.on_edit_file, idx)
 
367
            m.append(item)
 
368
            m.show_all()
 
369
            m.popup(None, None, None, event.button, event.time)
 
370
            return True
 
371
    def on_delete_link(self, menuitem, linked):
 
372
        idx = self.m_model.index(linked)
 
373
        if id(linked) in editor_of(self).m_page_mapping:
 
374
            editor_of(self).destroy_window(id(linked))
 
375
        self.g_link_box.get_children()[idx].destroy()
 
376
        del self.m_model[idx]
 
377
    def on_edit_linktext(self, menuitem, linked):
 
378
        idx = self.m_model.index(linked)
 
379
        # row is the hbox containing the linkbutton
 
380
        row = self.g_link_box.get_children()[idx]
 
381
        linkbutton = row.get_children()[0]
 
382
        entry = gtk.Entry()
 
383
        entry.set_text(linkbutton.get_label())
 
384
        row.pack_start(entry)
 
385
        linkbutton.hide()
 
386
        entry.show()
 
387
        entry.grab_focus()
 
388
        def finish_edit(entry):
 
389
            linkbutton.set_label(entry.get_text().decode("utf-8"))
 
390
            linkbutton.get_children()[0].set_alignment(0.0, 0.5)
 
391
            linkbutton.show()
 
392
            self.m_model[idx].m_name = entry.get_text().decode("utf-8")
 
393
            entry.destroy()
 
394
        sid = entry.connect('activate', finish_edit)
 
395
        def keydown(entry, event):
 
396
            if event.keyval == gtk.keysyms.Tab:
 
397
                finish_edit(entry)
 
398
        entry.connect('key-press-event', keydown)
 
399
        def keyup(entry, event):
 
400
            if event.keyval == gtk.keysyms.Escape:
 
401
                linkbutton.show()
 
402
                entry.disconnect(sid)
 
403
                entry.destroy()
 
404
                return True
 
405
        entry.connect('key-release-event', keyup)
 
406
    def on_edit_file(self, menuitem, linked):
 
407
        try:
 
408
            try:
 
409
                subprocess.call((cfg.get_string("programs/text-editor"),
 
410
                             lessonfile.uri_expand(self.m_model[linked])))
 
411
            except OSError, e:
 
412
                 raise osutils.BinaryForProgramException("Text editor", cfg.get_string("programs/text-editor"), e)
 
413
        except osutils.BinaryForProgramException, e:
 
414
            solfege.win.display_error_message2(e.msg1, e.msg2)
 
415
    def on_cut(self, btn):
 
416
        self.m_parent.cut_section(self)
 
417
    def on_cut_link(self, menuitem, idx):
 
418
        Editor.clipboard.append(self.m_model[idx])
 
419
        del self.m_model[idx]
 
420
        self.g_link_box.get_children()[idx].destroy()
 
421
    def on_paste(self, btn, idx):
 
422
        assert Editor.clipboard, "Paste buttons should be insensitive when the clipboard is empty."
 
423
        pobj = Editor.clipboard.pop()
 
424
        if isinstance(pobj, pd.LinkList):
 
425
            mobj = pd.Page(pobj.m_name, [pd.Column(pobj)])
 
426
        else:
 
427
            mobj = pobj
 
428
        if idx == -1:
 
429
            self.m_model.append(mobj)
 
430
            self.g_link_box.pack_start(self.create_linkrow(mobj))
 
431
        else:
 
432
            self.m_model.insert(idx, mobj)
 
433
            row = self.create_linkrow(mobj)
 
434
            self.g_link_box.pack_start(row)
 
435
            self.g_link_box.reorder_child(row, idx)
 
436
    def on_move_up(self, btn, idx):
 
437
        """
 
438
        Move the link one row up.
 
439
        """
 
440
        assert idx > 0
 
441
        self.m_model[idx], self.m_model[idx - 1] = self.m_model[idx - 1], self.m_model[idx]
 
442
        self.g_link_box.reorder_child(self.g_link_box.get_children()[idx], idx - 1)
 
443
    def on_move_down(self, btn, idx):
 
444
        """
 
445
        Move the link one row down.
 
446
        """
 
447
        self.m_model[idx], self.m_model[idx + 1] = self.m_model[idx + 1], self.m_model[idx]
 
448
        self.g_link_box.reorder_child(self.g_link_box.get_children()[idx], idx + 1)
 
449
 
 
450
 
 
451
class Column(gtk.VBox):
 
452
    def __init__(self, model, parent):
 
453
        gtk.VBox.__init__(self)
 
454
        self.set_spacing(gu.hig.SPACE_MEDIUM)
 
455
        self.m_model = model
 
456
        self.m_parent = parent
 
457
        assert isinstance(model, pd.Column)
 
458
        self.g_section_box = gtk.VBox()
 
459
        self.g_section_box.set_spacing(gu.hig.SPACE_MEDIUM)
 
460
        self.pack_start(self.g_section_box, False)
 
461
        for section in model:
 
462
            assert isinstance(section, pd.LinkList)
 
463
            gui_section = Section(section, self)
 
464
            self.g_section_box.pack_start(gui_section, False)
 
465
        hbox = gtk.HBox()
 
466
        self.pack_start(hbox, False)
 
467
        b = gtk.Button(_("Add section"))
 
468
        hbox.pack_start(b, False)
 
469
        b.connect('clicked', self.on_add_section)
 
470
        b = gtk.Button(stock=gtk.STOCK_PASTE)
 
471
        b.connect('clicked', self.on_paste)
 
472
        Editor.clipboard.register_paste_button(b, pd.LinkList)
 
473
        hbox.pack_start(b, False)
 
474
    def __del__(self):
 
475
        logging.debug("Column.__del__")
 
476
    def cut_section(self, section):
 
477
        idx = self.g_section_box.get_children().index(section)
 
478
        Editor.clipboard.append(self.m_model[idx])
 
479
        del self.m_model[idx]
 
480
        self.g_section_box.get_children()[idx].destroy()
 
481
    def remove_section(self, section):
 
482
        idx = self.g_section_box.get_children().index(section)
 
483
        del self.m_model[idx]
 
484
        self.g_section_box.get_children()[idx].destroy()
 
485
    def on_add_section(self, btn):
 
486
        # We write "Untitled%s" % "" instead of just "Untitled" here
 
487
        # since "Untitled%s" is already translated in many languages.
 
488
        section = pd.LinkList(_("Untitled%s" % ""))
 
489
        self.m_model.append(section)
 
490
        gui_section = Section(section, self)
 
491
        self.g_section_box.pack_start(gui_section, False)
 
492
        gui_section.show_all()
 
493
    def move_section_down(self, section):
 
494
        idx = self.g_section_box.get_children().index(section)
 
495
        if idx < len(self.g_section_box.get_children()) - 1:
 
496
            self.g_section_box.reorder_child(section, idx + 1)
 
497
            self.m_model[idx], self.m_model[idx + 1] \
 
498
                    = self.m_model[idx + 1], self.m_model[idx]
 
499
            self.m_parent.update_buttons()
 
500
    def move_section_up(self, section):
 
501
        idx = self.g_section_box.get_children().index(section)
 
502
        if idx > 0:
 
503
            self.g_section_box.reorder_child(section, idx - 1)
 
504
            self.m_model[idx], self.m_model[idx - 1] \
 
505
                    = self.m_model[idx - 1], self.m_model[idx]
 
506
            self.m_parent.update_buttons()
 
507
    def on_paste(self, widget):
 
508
        """
 
509
        Paste the clipboard as a new section to this column.
 
510
        """
 
511
        assert Editor.clipboard, "Paste buttons should be insensitive when the clipboard is empty."
 
512
        assert isinstance(Editor.clipboard[-1], pd.LinkList)
 
513
        pobj = Editor.clipboard.pop()
 
514
        self.m_model.append(pobj)
 
515
        sect = Section(pobj, self)
 
516
        sect.show_all()
 
517
        self.g_section_box.pack_start(sect, False)
 
518
 
 
519
 
 
520
class Page(gtk.VBox):
 
521
    def __init__(self, model, parent):
 
522
        gtk.VBox.__init__(self)
 
523
        self.m_model = model
 
524
        self.m_parent = parent
 
525
        sc = gtk.ScrolledWindow()
 
526
        sc.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
527
        self.pack_start(sc)
 
528
        self.g_column_box = gtk.HBox()
 
529
        self.g_column_box.set_spacing(gu.hig.SPACE_LARGE)
 
530
        self.g_column_box.set_border_width(gu.hig.SPACE_SMALL)
 
531
        # We pack column into this box
 
532
        sc.add_with_viewport(self.g_column_box)
 
533
        self.show_all()
 
534
        if model:
 
535
            self.update_from_model()
 
536
    def __del__(self):
 
537
        logging.debug("Page.__del__:", self.m_model.m_name)
 
538
    def on_add_column(self, *btn):
 
539
        column = pd.Column()
 
540
        self.m_model.append(column)
 
541
        gcol = Column(column, self)
 
542
        gcol.show_all()
 
543
        self.g_column_box.pack_start(gcol)
 
544
    def on_move_section_left(self, button, section):
 
545
        column_idx = self.g_column_box.get_children().index(section.m_parent)
 
546
        section_idx = section.m_parent.g_section_box.get_children().index(section)
 
547
        if column_idx > 0:
 
548
            to_column = self.g_column_box.get_children()[column_idx - 1]
 
549
            section.reparent(to_column.g_section_box)
 
550
            section.m_parent = to_column
 
551
            to_column.g_section_box.set_child_packing(section, False, False, 0, gtk.PACK_START)
 
552
            self.m_model[column_idx - 1].append(self.m_model[column_idx][section_idx])
 
553
            del self.m_model[column_idx][section_idx]
 
554
            # Remove the right-most column if we moved the
 
555
            # last section out of it.
 
556
            if not self.g_column_box.get_children()[-1].g_section_box.get_children():
 
557
                assert len(self.m_model[-1]) == 0
 
558
                del self.m_model[-1]
 
559
                self.g_column_box.get_children()[-1].destroy()
 
560
            self.update_buttons()
 
561
    def on_move_section_right(self, button, section):
 
562
        # the column we move from
 
563
        column_idx = self.g_column_box.get_children().index(section.m_parent)
 
564
        section_idx = section.m_parent.g_section_box.get_children().index(section)
 
565
        if column_idx == len(self.g_column_box.get_children()) - 1:
 
566
            self.on_add_column()
 
567
        to_column = self.g_column_box.get_children()[column_idx + 1]
 
568
        section.reparent(to_column.g_section_box)
 
569
        section.m_parent = to_column
 
570
        to_column.g_section_box.set_child_packing(section, False, False, 0, gtk.PACK_START)
 
571
        to_section_idx = len(self.m_model[column_idx + 1])
 
572
        self.m_model[column_idx + 1].append(self.m_model[column_idx][section_idx])
 
573
        del self.m_model[column_idx][section_idx]
 
574
        self.update_buttons()
 
575
    def update_from_model(self):
 
576
        for child in self.g_column_box.get_children():
 
577
            child.destroy()
 
578
        for column in self.m_model:
 
579
            self.g_column_box.pack_start(Column(column, self))
 
580
        self.g_column_box.show_all()
 
581
        self.update_buttons()
 
582
    def update_buttons(self):
 
583
        num_cols = len(self.g_column_box.get_children())
 
584
        for col_idx, column in enumerate(self.g_column_box.get_children()):
 
585
            num_sects = len(column.g_section_box.get_children())
 
586
            for sect_idx, section in enumerate(column.g_section_box.get_children()):
 
587
                section.g_move_up_btn.set_sensitive(sect_idx != 0)
 
588
                section.g_move_down_btn.set_sensitive(sect_idx != num_sects -1)
 
589
                section.g_move_left_btn.set_sensitive(col_idx != 0)
 
590
                if [col for col in self.g_column_box.get_children() if not col.g_section_box.get_children()] and col_idx == num_cols - 1:
 
591
                    section.g_move_right_btn.set_sensitive(False)
 
592
                else:
 
593
                    section.g_move_right_btn.set_sensitive(True)
 
594
 
 
595
 
 
596
class Clipboard(list):
 
597
    def __init__(self, v=[]):
 
598
        list.__init__(v)
 
599
        self.m_paste_buttons = []
 
600
    def pop(self, i=-1):
 
601
        ret = list.pop(self, i)
 
602
        self.update_buttons()
 
603
        return ret
 
604
    def append(self, obj):
 
605
        list.append(self, obj)
 
606
        self.update_buttons()
 
607
    def register_paste_button(self, button, accepts_types):
 
608
        button.set_sensitive(bool(self) and isinstance(self[-1], types))
 
609
        self.m_paste_buttons.append((button, accepts_types))
 
610
    def update_buttons(self):
 
611
        for button, types in self.m_paste_buttons:
 
612
            button.set_sensitive(bool(self) and isinstance(self[-1], types))
 
613
 
 
614
 
 
615
class Editor(gtk.Window, gu.EditorDialogBase):
 
616
    savedir = os.path.join(filesystem.user_data(), u'exercises', u'user')
 
617
    # The clipboard will be shared between all Editor instances
 
618
    clipboard = Clipboard()
 
619
    def __init__(self, filename=None):
 
620
        gtk.Window.__init__(self)
 
621
        logging.debug("fpeditor.Editor.__init__(%s)" % filename)
 
622
        gu.EditorDialogBase.__init__(self, filename)
 
623
        self.set_default_size(800, 600)
 
624
        self.g_main_box = gtk.VBox()
 
625
        self.add(self.g_main_box)
 
626
        self.g_actiongroup.add_actions([
 
627
            ('GoBack', gtk.STOCK_GO_BACK, None, None, None, self.go_back),
 
628
        ])
 
629
        self.setup_toolbar()
 
630
        self.g_title_hbox = gtk.HBox()
 
631
        self.g_title_hbox.set_spacing(gu.hig.SPACE_SMALL)
 
632
        self.g_title_hbox.set_border_width(gu.hig.SPACE_SMALL)
 
633
        label = gtk.Label()
 
634
        label.set_markup(u"<b>%s</b>" % _("Front page title:"))
 
635
        self.g_title_hbox.pack_start(label, False)
 
636
        self.g_fptitle = gtk.Entry()
 
637
        self.g_title_hbox.pack_start(self.g_fptitle)
 
638
        self.g_main_box.pack_start(self.g_title_hbox, False)
 
639
        # This dict maps the windows created for all pages belonging to
 
640
        # the file.
 
641
        self.m_page_mapping = {}
 
642
        self.m_model = None
 
643
        if filename:
 
644
            self.load_file(filename)
 
645
        else:
 
646
            self.m_model = pd.Page(_("Untitled%s") % self.m_instance_number,
 
647
                    pd.Column())
 
648
            self.set_not_modified()
 
649
        self.add_page(Page(self.m_model, self))
 
650
        self.clipboard.update_buttons()
 
651
        self.show_all()
 
652
        self.add_to_instance_dict()
 
653
        self.g_fptitle.set_text(self.m_model.m_name)
 
654
        self.g_fptitle.connect('changed', self.on_frontpage_title_changed)
 
655
    def __del__(self):
 
656
        logging.debug("fpeditor.Editor.__del__, filename=" % self.m_filename)
 
657
    def add_page(self, page):
 
658
        """
 
659
        Add and show the page.
 
660
        """
 
661
        editor_of(self).m_page_mapping[id(page.m_model)] = page
 
662
        self.g_main_box.pack_start(page)
 
663
        self.show_page(page)
 
664
    def show_page_id(self, page_id):
 
665
        self.show_page(self.m_page_mapping[page_id])
 
666
    def show_page(self, page):
 
667
        """
 
668
        Hide the currently visible page, and show PAGE instead.
 
669
        """
 
670
        try:
 
671
            self.g_visible_page.hide()
 
672
        except AttributeError:
 
673
            pass
 
674
        self.g_visible_page = page
 
675
        page.show()
 
676
        if isinstance(page.m_parent, Page):
 
677
            self.g_title_hbox.hide()
 
678
        else:
 
679
            self.g_title_hbox.show()
 
680
        self.g_ui_manager.get_widget("/Toolbar/GoBack").set_sensitive(
 
681
            not isinstance(self.g_visible_page.m_parent, Editor))
 
682
    def go_back(self, *action):
 
683
        self.show_page(self.g_visible_page.m_parent)
 
684
    def on_frontpage_title_changed(self, widget):
 
685
        self.m_model.m_name = widget.get_text()
 
686
    def setup_toolbar(self):
 
687
        self.g_ui_manager.insert_action_group(self.g_actiongroup, 0)
 
688
        uixml = """
 
689
        <ui>
 
690
         <toolbar name='Toolbar'>
 
691
          <toolitem action='GoBack'/>
 
692
          <toolitem action='New'/>
 
693
          <toolitem action='Open'/>
 
694
          <toolitem action='Save'/>
 
695
          <toolitem action='SaveAs'/>
 
696
          <toolitem action='Close'/>
 
697
          <toolitem action='Help'/>
 
698
         </toolbar>
 
699
         <accelerator action='Close'/>
 
700
         <accelerator action='New'/>
 
701
         <accelerator action='Open'/>
 
702
         <accelerator action='Save'/>
 
703
        </ui>
 
704
        """
 
705
        self.g_ui_manager.add_ui_from_string(uixml)
 
706
        toolbar = self.g_ui_manager.get_widget("/Toolbar")
 
707
        self.g_main_box.pack_start(toolbar, False)
 
708
        self.g_main_box.reorder_child(toolbar, 0)
 
709
        self.g_ui_manager.get_widget("/Toolbar").set_style(gtk.TOOLBAR_BOTH)
 
710
    def destroy_window(self, window_id):
 
711
        """
 
712
        Destroy the window with the id 'windowid' and all subwindows.
 
713
        """
 
714
        def do_del(wid):
 
715
            for key in [k for k in self.m_page_mapping.keys()
 
716
                    if id(parent_page(self.m_page_mapping[k]).m_model) == wid]:
 
717
                do_del(key)
 
718
            editor_of(self).m_page_mapping[wid].destroy()
 
719
            del editor_of(self).m_page_mapping[wid]
 
720
        do_del(window_id)
 
721
    @staticmethod
 
722
    def edit_file(fn):
 
723
        if fn in Editor.instance_dict:
 
724
            Editor.instance_dict[fn].present()
 
725
        else:
 
726
            try:
 
727
                win = Editor(fn)
 
728
                win.show()
 
729
            except IOError, e:
 
730
                gu.dialog_ok(_("Loading file '%(filename)s' failed: %(msg)s") %
 
731
                        {'filename': fn, 'msg': str(e).decode('utf8', 'replace')})
 
732
    def load_file(self, filename):
 
733
        """
 
734
        Load a file into a empty, newly created Editor object.
 
735
        """
 
736
        assert self.m_model == None
 
737
        self.m_model = pd.load_tree(filename, C_locale=True)
 
738
        self.m_filename = filename
 
739
        # 
 
740
        if not os.path.isabs(filename):
 
741
            if not os.access(filename, os.W_OK):
 
742
                m = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO,
 
743
                    gtk.BUTTONS_CLOSE, _("The default learning tree is write protected in your install. This is normal. If you want to edit a learning tree, you have to select one of the trees stored in .solfege/learningtrees in your home directory."))
 
744
                m.run()
 
745
                m.destroy()
 
746
        self.set_not_modified()
 
747
        self.set_title(self.m_filename)
 
748
    def set_not_modified(self):
 
749
        """
 
750
        Store the current state of the data in self.m_orig_dump so that
 
751
        is_modified() will return False until we make new changes.
 
752
        """
 
753
        io = StringIO.StringIO()
 
754
        self.m_model.dump(io)
 
755
        self.m_orig_dump = io.getvalue()
 
756
    def is_modified(self):
 
757
        """
 
758
        Return True if the data has changed since the last call to
 
759
        set_not_modified()
 
760
        """
 
761
        io = StringIO.StringIO()
 
762
        self.m_model.dump(io)
 
763
        s = io.getvalue()
 
764
        return s != self.m_orig_dump
 
765
    @property
 
766
    def m_changed(self):
 
767
        return self.is_modified()
 
768
    def save(self, w=None):
 
769
        assert self.m_filename
 
770
        save_location = os.path.split(self.m_filename)[0] + os.sep
 
771
        fh = pd.FileHeader(1, self.m_model)
 
772
        fh.save_file(self.m_filename)
 
773
        self.set_not_modified()
 
774
        # We do test for solfege.win since it is not available during testing
 
775
        if getattr(solfege, 'win', None):
 
776
            solfege.win.on_frontpage_changed()
 
777
    def on_show_help(self, *w):
 
778
        return
 
779
    def get_save_as_dialog(self):
 
780
        dialog = gu.EditorDialogBase.get_save_as_dialog(self)
 
781
        ev2 = gtk.EventBox()
 
782
        ev2.set_name("DIALOGWARNING2")
 
783
        ev = gtk.EventBox()
 
784
        ev.set_border_width(gu.hig.SPACE_SMALL)
 
785
        ev2.add(ev)
 
786
        ev.set_name("DIALOGWARNING")
 
787
        label = gtk.Label()
 
788
        label.set_padding(gu.hig.SPACE_MEDIUM, gu.hig.SPACE_MEDIUM)
 
789
        ev.add(label)
 
790
        label.set_markup(_("<b>IMPORTANT:</b> Your front page file <b>must</b> be saved in a subdirectory below the directory named exercises. See the user manual for details."))
 
791
        dialog.set_extra_widget(ev2)
 
792
        ev2.show_all()
 
793
        return dialog
 
794
 
 
795
if __name__ == '__main__':
 
796
    gtk.link_button_set_uri_hook(lambda a, b: None)
 
797
    e = Editor()
 
798
    e.load_file("learningtrees/learningtree.txt")
 
799
    gtk.main()