pgconsole/editor.py
author Radek Brich <radek.brich@devl.cz>
Tue, 04 Feb 2014 16:10:04 +0100
changeset 94 a10f553e6f6a
parent 76 3a41b351b122
permissions -rw-r--r--
PgDiff: Add patch support for SQL functions.

from gi.repository import Gtk
from gi.repository import Pango
from gi.repository import GtkSource

from lxml import etree

from pgconsole.panedext import HPanedExt
from pgconsole.config import cfg


class Editor(HPanedExt):
    def __init__(self):
        super(Editor, self).__init__()

        self.view = GtkSource.View()
        self.view.connect('toggle-overwrite', self.on_toggle_overwrite)

        vbox = Gtk.VBox()
        self.vbox1 = vbox
        self.add1(vbox)

        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        sw.set_shadow_type(Gtk.ShadowType.IN)
        vbox.pack_start(sw, expand=True, fill=True, padding=0)
        tree = Gtk.TreeView()
        tree.set_headers_visible(False)
        tree.get_selection().set_mode(Gtk.SelectionMode.BROWSE)
        sw.add(tree)

        model = Gtk.ListStore(str, str, object, bool)  # title, filename, buffer, modified
        tree.set_model(model)
        cell = Gtk.CellRendererPixbuf()
        cell.set_property('stock-id', Gtk.STOCK_SAVE)
        column = Gtk.TreeViewColumn("File", cell, visible=3)
        tree.append_column(column)
        column = Gtk.TreeViewColumn("File", Gtk.CellRendererText(), text=0)
        tree.append_column(column)
        tree.get_selection().connect('changed', self.item_change)
        tree.set_property('can-focus', False)
        self.filelist = tree


        hbox = Gtk.HBox()

        img = Gtk.Image()
        img.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.SMALL_TOOLBAR)
        btn = Gtk.Button()
        btn.set_relief(Gtk.ReliefStyle.NONE)
        btn.set_focus_on_click(False)
        btn.set_image(img)
        btn.connect('clicked', self.item_new)
        hbox.pack_start(btn, expand=False, fill=True, padding=0)

        img = Gtk.Image()
        img.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.SMALL_TOOLBAR)
        btn = Gtk.Button()
        btn.set_relief(Gtk.ReliefStyle.NONE)
        btn.set_focus_on_click(False)
        btn.set_image(img)
        btn.connect('clicked', self.item_open)
        hbox.pack_start(btn, expand=False, fill=True, padding=0)

        img = Gtk.Image()
        img.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.SMALL_TOOLBAR)
        btn = Gtk.Button()
        btn.set_relief(Gtk.ReliefStyle.NONE)
        btn.set_focus_on_click(False)
        btn.set_image(img)
        btn.connect('clicked', self.item_close)
        hbox.pack_start(btn, expand=False, fill=True, padding=0)
        hbox.connect('size-allocate', self.leftbuttons_size_request)

        vbox.pack_start(hbox, expand=False, fill=True, padding=0)

        vbox = Gtk.VBox()
        vbox.set_property("width-request", 200)
        self.add2(vbox)
        self.child_set_property(vbox, 'shrink', False)


        # scroll
        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.add(self.view)
        vbox.pack_start(sw, expand=True, fill=True, padding=0)


        self.view.set_show_line_numbers(True)
        self.view.set_smart_home_end(True)
        #self.view.set_insert_spaces_instead_of_tabs(True)
        self.view.set_property("tab-width", 4)
        self.view.set_auto_indent(True)

        # font
        font_desc = Pango.FontDescription('monospace')
        if font_desc:
            self.view.modify_font(font_desc)

        # status bar
        hbox = Gtk.HBox()

        self.st_file, fr1 = self.construct_status('SQL snippet')
        self.st_file.set_ellipsize(Pango.EllipsizeMode.START)
        self.st_insovr, fr2 = self.construct_status('INS')
        self.st_linecol, fr3 = self.construct_status('Line: 0 Col: 0')


        img = Gtk.Image()
        img.set_from_stock(Gtk.STOCK_SAVE, Gtk.IconSize.SMALL_TOOLBAR)
        #save = img
        save = Gtk.Button()
        save.set_relief(Gtk.ReliefStyle.NONE)
        save.set_focus_on_click(False)
        save.set_image(img)
        save.connect('clicked', self.item_save)
        hbox.pack_start(save, expand=False, fill=True, padding=0)

        hbox.pack_start(fr1, expand=True, fill=True, padding=0)
        hbox.pack_start(fr2, expand=False, fill=True, padding=0)
        hbox.pack_start(fr3, expand=False, fill=True, padding=0)

        #sep = Gtk.HSeparator()
        #vbox.pack_start(sep, expand=False, fill=False, padding=0)

        #align = Gtk.Alignment()
        #align.set_property("bottom-padding", 3)
        #align.set_property("xscale", 1.0)
        #align.add(hbox)

        #frame = Gtk.Frame()
        #frame.add(hbox)
        #frame.set_shadow_type(Gtk.SHADOW_ETCHED_IN)
        vbox.pack_start(hbox, expand=False, fill=True, padding=0)

        self.load_nodes()
        self.build_context_menu()


    def build_context_menu(self):
        menu = Gtk.Menu()
        item = Gtk.ImageMenuItem(use_stock=True, label=Gtk.STOCK_SAVE)
        item.connect("activate", self.item_save)
        item.show()
        menu.append(item)

        item = Gtk.ImageMenuItem(use_stock=True, label=Gtk.STOCK_SAVE_AS)
        item.connect("activate", self.item_save_as)
        item.show()
        menu.append(item)

        item = Gtk.ImageMenuItem(use_stock=True, label=Gtk.STOCK_CLOSE)
        item.connect("activate", self.item_close)
        item.show()
        menu.append(item)

        item = Gtk.SeparatorMenuItem()
        item.show()
        menu.append(item)

        item = Gtk.ImageMenuItem(use_stock=True, label=Gtk.STOCK_GO_UP)
        item.show()
        menu.append(item)

        item = Gtk.ImageMenuItem(use_stock=True, label=Gtk.STOCK_GO_DOWN)
        item.show()
        menu.append(item)

        self.filelist_menu = menu

        self.filelist.connect_object("button-press-event", self.on_filelist_button_press_event, menu)


    def on_filelist_button_press_event(self, w, event):
        if event.button == 3:
            x = int(event.x)
            y = int(event.y)
            pathinfo = self.filelist.get_path_at_pos(x, y)
            if pathinfo is not None:
                path, col, cellx, celly = pathinfo
                self.filelist.grab_focus()
                self.filelist.set_cursor(path, col, 0)
                self.filelist_menu.popup(None, None, None, None, event.button, event.time)
            return True



    def make_buffer(self):
        buffer = GtkSource.Buffer()
        buffer.connect('mark-set', self.buffer_mark_set)
        buffer.connect('changed', self.buffer_changed)

        # style
        mgr = GtkSource.StyleSchemeManager.get_default()
        style_scheme = mgr.get_scheme('kate')
        if style_scheme:
            buffer.set_style_scheme(style_scheme)

        # syntax
        lngman = GtkSource.LanguageManager.get_default()
        langsql = lngman.get_language('sql')
        buffer.set_language(langsql)
        buffer.set_highlight_syntax(True)

        return buffer


    def load_nodes(self):
        model = self.filelist.get_model()
        sel = cfg.root.editor.nodes.get('selected')
        for node in cfg.root.editor.nodes.node:
            buffer = self.make_buffer()
            name = node.get('name')
            type = node.get('type')
            filename = None
            if type == 'text' and node.text:
                buffer.set_text(node.text)
            if type == 'file':
                filename = node.text
                try:
                    content = open(filename).read()
                except IOError:
                    content = ''
                buffer.set_text(content)
            iter = model.append([name, filename, buffer, False])
            if sel == name:
                self.filelist.get_selection().select_iter(iter)


    def set_text(self, text):
        self.buffer.set_text(text)


    def get_text(self):
        start, end = self.buffer.get_bounds()
        return self.buffer.get_text(start, end)


    def get_selection(self):
        bounds = self.buffer.get_selection_bounds()
        if not bounds:
            return None
        return self.buffer.get_text(*bounds)


    def construct_status(self, text):
        st = Gtk.Label(text)
        st.set_property("single-line-mode", True)
        st.set_property("xpad", 3)
        st.set_alignment(0.0, 0.5)
        fr = Gtk.Frame()
        fr.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
        fr.add(st)
        return st, fr


    def on_toggle_overwrite(self, w):
        if not w.get_overwrite():
            self.st_insovr.set_label('OVR')
        else:
            self.st_insovr.set_label('INS')


    def buffer_mark_set(self, w, iter, textmark):
        if textmark == w.get_insert():
            line = iter.get_line()
            col = iter.get_visible_line_offset()
            self.st_linecol.set_label('Line: %d Col: %d' % (line + 1, col + 1))


    def buffer_changed(self, w):
        iter = w.get_iter_at_mark(w.get_insert())
        line = iter.get_line()
        col = iter.get_visible_line_offset()
        self.st_linecol.set_label('Line: %d Col: %d' % (line + 1, col + 1))


    def item_change(self, w):
        model, sel = self.filelist.get_selection().get_selected_rows()
        if not sel:
            return
        iter = model.get_iter(sel[0])
        title, filename, self.buffer = model.get(iter, 0, 1, 2)
        self.view.set_buffer(self.buffer)

        if filename:
            self.st_file.set_text(filename)
        else:
            self.st_file.set_text('SQL snippet')

        # update config
        cfg.root.editor.nodes.set('selected', title)


    def probe_title(self, model, title):
        iter = model.get_iter_first()
        i = 1
        while iter:
            if model.get_value(iter, 0).split(' /')[0] == title:
                new = title
                if i > 1:
                    new += ' /%d' % i
                model.set_value(iter, 0, new)
                i += 1
            iter = model.iter_next(iter)
        if i > 1:
            title = '%s /%d' % (title, i)
        return title


    def item_new(self, w):
        model = self.filelist.get_model()
        title = 'Untitled'
        title = self.probe_title(model, title)
        buffer = self.make_buffer()
        iter = model.append([title, None, buffer, False])
        self.filelist.get_selection().select_iter(iter)

        # update config
        etree.SubElement(cfg.root.editor.nodes, 'node', type='text', name=title)
        cfg.root.editor.nodes.set('selected', title)
        cfg.save()


    def item_open(self, w):
        dialog = Gtk.FileChooserDialog(title='Open file', action=Gtk.FileChooserAction.OPEN,
            buttons=(Gtk.STOCK_CANCEL,Gtk.ResponseType.CANCEL,Gtk.STOCK_OPEN,Gtk.ResponseType.OK))
        dialog.set_default_response(Gtk.ResponseType.OK)
        dialog.set_select_multiple(True)

        filter = Gtk.FileFilter()
        filter.set_name("SQL files")
        filter.add_pattern("*.sql")
        dialog.add_filter(filter)

        filter = Gtk.FileFilter()
        filter.set_name("All files")
        filter.add_pattern("*")
        dialog.add_filter(filter)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            filenames = dialog.get_filenames()
            for fname in filenames:
                self.open_file(fname)

        dialog.destroy()


    def open_file(self, filename):
        title = filename.rsplit('/', 1)[1]

        model = self.filelist.get_model()
        iter = model.get_iter_first()
        i = 1
        while iter:
            if model.get_value(iter, 1) == filename:
                # file already opened, select it
                self.filelist.get_selection().select_iter(iter)
                return
            iter = model.iter_next(iter)

        title = self.probe_title(model, title)

        # add item
        buffer = self.make_buffer()
        buffer.set_text(open(filename).read())
        iter = model.append([title, filename, buffer, False])
        self.filelist.get_selection().select_iter(iter)

        # update config
        el = etree.SubElement(cfg.root.editor.nodes, 'node', type='file', name=title)
        el._setText(filename)
        cfg.root.editor.nodes.set('selected', title)
        cfg.save()


    def item_save(self, w):
        model, sel = self.filelist.get_selection().get_selected_rows()
        if not sel:
            return
        iter = model.get_iter(sel[0])

        filename, buffer = model.get(iter, 1, 2)

        data = buffer.get_text(*buffer.get_bounds())
        open(filename, 'w').write(data)


    def item_save_as(self, w):
        model, sel = self.filelist.get_selection().get_selected_rows()
        if not sel:
            return
        iter = model.get_iter(sel[0])

        filename, buffer = model.get(iter, 1, 2)

        path, file = filename.rsplit('/',1)

        dialog = Gtk.FileChooserDialog(title='Save as', action=Gtk.FileChooserAction.SAVE,
            buttons=(Gtk.STOCK_CANCEL,Gtk.ResponseType.CANCEL,Gtk.STOCK_SAVE,Gtk.ResponseType.OK))
        dialog.set_default_response(Gtk.ResponseType.OK)
        dialog.set_current_folder(path)
        dialog.set_current_name(file)

        filter = Gtk.FileFilter()
        filter.set_name("SQL files")
        filter.add_pattern("*.sql")
        dialog.add_filter(filter)

        filter = Gtk.FileFilter()
        filter.set_name("All files")
        filter.add_pattern("*")
        dialog.add_filter(filter)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            filename = dialog.get_filename()
            data = buffer.get_text(*buffer.get_bounds())
            open(filename, 'w').write(data)

            title = filename.rsplit('/',1)[1]
            title = self.probe_title(model, title)
            model.set(iter, 0, title)
            model.set(iter, 1, filename)
            model.set(iter, 3, False) # modified

        dialog.destroy()


    def item_close(self, w):
        model, sel = self.filelist.get_selection().get_selected_rows()
        if not sel:
            return
        iter = model.get_iter(sel[0])
        newiter = model.iter_next(iter)
        if newiter is None and sel[0].get_indices()[0] > 0:
            newiter = model.get_iter((sel[0].get_indices()[0]-1,))

        title, buffer = model.get(iter, 0, 2)
        #buffer.destroy()

        model.remove(iter)

        if newiter is None:
            self.item_new(None)
        else:
            self.filelist.get_selection().select_iter(newiter)

        # update config
        el = cfg.root.editor.nodes.find('node[@name="%s"]' % title)
        el.getparent().remove(el)
        cfg.save()


    def leftbuttons_size_request(self, w, request):
        self.set_snap1(request.width)
        return True