AUI in WxRuby

A lot has happened since the last part. As far as I’m concerned the framework is basically finished, what remains now is to create a few more lexers, for CSS, HTML and Javascript primarily. I’ve also tried to create my own integrated lexer (Pico instead of Common Lisp) in the Scintilla component, more on that as it develops…


Currently we have:

- Autocompletion with local, global and uberglobal scope. A whole project is now parsed recursively to create a list of classes, their methods and global functions and variables.

- Instant code jumping to the definitions of parsed words.

- Partial string search in the whole project and in the current file, results display in a new tree with each file/line the partial is found in.

- Project browsing with an arbitrary number of projects, with the following hierarchy: Project -> folder -> file -> globals/classes -> methods. Additional folders (libraries) can be added to each project.

- The workspace is saved on quit and loaded again automatically the next time the editor is started.

Basically most of what I love about Eclipse is there:

Project browser

As you can see the layout is horizontal, I work with dual wide screen monitors and don’t like when search results etc open up some kind of widget at the bottom of the screen further constricting my already crappy vertical space. That’s why everything happens to the left:

Search result

Icons from the Buuf set by mattahan. I simply love them.

Most pressing TODO:

- Replace in document/project.

- Rebuild project tree/keyword list.

What we have here is an AUI interface with two AuiNotebooks which are responsible for handling the tabbing, one to keep track of the project tree and search results and the other to manage open documents. Each open document is a Scintilla instance.

Let’s go through the AUI code that serves as the entry point for the application (editor.rb):

begin
  require 'rubygems' 
rescue LoadError
end
require 'wx'
include Wx
require 'yaml'
require 'lexer.rb'
require 'scintilla.rb'
require 'treectrl.rb'

Minimal_Quit = 1
Minimal_About = ID_ABOUT
Toggle_Whitespace = 5000
Toggle_EOL = 5001

class AuiFrame < Wx::Frame
  def initialize(*args)
    super
    @mgr = Wx::AuiManager.new
    @mgr.set_managed_window(self)

    @notebook_style = Wx::AUI_NB_DEFAULT_STYLE|
      Wx::AUI_NB_TAB_EXTERNAL_MOVE|Wx::NO_BORDER
    @notebook_theme = 0
    @pr_visible = true
    setup_menu
    set_min_size( Wx::Size.new(400,300) )
    setup_panes
    setup_events
    @mgr.update
  end

This code is based on the AUI sample and as you can see we have transferred some stuff from the frame in the scintilla example. The AuiManager is responsible for controlling our panes (two notebooks). @pr_visible keeps track of whether the left pane is visible or not, it can be toggled on or off (more on that later).

def setup_events
    evt_close {|event| onQuit }
    evt_menu(Minimal_Quit) {onQuit}
    evt_menu(Toggle_Whitespace) {onWhitespace}
    evt_menu(Toggle_EOL) {onEOL}
    evt_menu(6000) {onAutoComplete}
    evt_menu(6001) {onJumpTo}
    evt_menu(6002) {onToggleBrowser}
    evt_menu(6003) {onSaveFile}
    evt_menu(6004) {onSearchDocument}
    evt_menu(6005) {onSearchProject}
    evt_auinotebook_page_close(Wx::ID_ANY) { | e | on_notebook_page_close(e) }
    evt_auinotebook_page_changed(Wx::ID_ANY) { | e | on_notebook_page_changed(e) }
  end
  
  def setup_menu
    mb = Wx::MenuBar.new

    menuView = Menu.new()
    menuView.append(Toggle_Whitespace, "Show &Whitespace\tF6", "Show Whitespace", ITEM_CHECK)
    menuView.append(6000, "Show autocomplete\tCtrl-Space", "Show autocomplete")
    menuView.append(6001, "Jump To\tF3", "Jump To")
    menuView.append(6002, "Toggle Project Browser\tCtrl-P", "Toggle Browser")
    
    searchMenu = Menu.new()
    searchMenu.append(6004, "&Find in Document\tCtrl-F", "Find in Document")
    searchMenu.append(6005, "Find in Project\tCtrl-H", "Find in Project")
    
    fileMenu = Wx::Menu.new
    fileMenu.append(Minimal_Quit, "E&xit\tAlt-X", "Quit this program")
    fileMenu.append(6003, "Save File\tCtrl-S", "Save File")

    mb.append(fileMenu, "&File")
    mb.append(searchMenu, "&Find")
    mb.append(menuView, "&View")

    set_menu_bar(mb)
  end
  
  def setup_panes
    @page_bmp = Wx::ArtProvider::get_bitmap( Wx::ART_NORMAL_FILE, 
                                            Wx::ART_OTHER, 
                                            Wx::Size.new(16,16) )
                                          
    pi = Wx::AuiPaneInfo.new
    pi.set_name('tree_content').left
    pi.set_layer(1).set_position(1)
    @mgr.add_pane(create_treebook, pi)
    
    pi = Wx::AuiPaneInfo.new
    pi.set_name('notebook_content').center_pane
    pi.set_layer(1).set_position(1)
    @mgr.add_pane(create_notebook, pi)
    
    @project_tree.loadWorkspace
  end

Note the use of evt_close in order to save the workspace on quit. We also keep track of when we close and switch pages in the right notebook.

create_treebook is responsible for creating the project browser which is a TreeCtrl, the result is @project_tree which will finish off by loading the workspace (more on our special tree control in the next part…).

def create_book(sx, sy)
    client_size = get_client_size
    Wx::AuiNotebook.new( self, Wx::ID_ANY,
                                Wx::Point.new(client_size.x, client_size.y),
                                Wx::Size.new(sx, sy),
                                @notebook_style)
  end
  
  def create_treebook
    @treebook = self.create_book(150, 200)
    @treebook.add_page(self.create_proj_tree, "Projects", true)
    return @treebook
  end
  
  def create_notebook
    @notebook = self.create_book(430, 200)
  end
  
  def create_text_ctrl(path, proj_id, lexer = nil)
    if path
      lexer = :pico unless lexer
      Scintilla.new(self, lexer, path, @project_tree, proj_id)
    else
      puts "there is no support for creating new documents"
    end
  end
  
  def create_tree(tpl = false, type = :file_browser)
    @cur_tree = ProjTree.new(self, tpl, type)
  end
  
  def create_proj_tree
    @project_tree = self.create_tree
  end

ProjTree and Scintilla are our extensions to the wxRuby defaults. Notice the use of :pico as the default lexer, creating some kind of default lexer and using that one is on the todo list. Also note that there is no functionality for deleting, renaming and creating new documents, I really don’t see the point in that, the convenience factor is minimal compared to just using your favorite file explorer in a separate window.

The ProjTree constructor can take another tree as input, this is the basic mechanism behind the resultant trees when we search for instance. In that case we just pass the project tree to the new tree so that it can use its data when generating its own content.

def on_notebook_page_changed(event)
    if(@notebook == event.get_event_object)
      self.setCurSci
    end
  end
  
  def setCurSci
    @sci = @notebook.get_page(@notebook.get_selection)
  end

Simple way of always making sure we have the current Scintilla instance handy in @sci.

def onSaveFile
    @sci.onSaveFile
  end
  
  def saveWorkspace
    workspace = (0..@notebook.get_page_count).collect do |n|
      if sci = @notebook.get_page(n)
        {:path => sci.file_path, :line_nbr => sci.get_current_line}
      end
    end
    File.open("workspace.yaml", "w"){|f| YAML.dump(workspace.compact, f)}
  end
  
  def onQuit
    self.saveWorkspace
    destroy
    exit
  end

  def onAutoComplete
    @sci.onAutoComplete
  end

When the software is killed we save the currently open files in a YAML config file so that they can be retrieved next time we start.

def onToggleBrowser
    if @pr_visible = !@pr_visible
      @mgr.get_pane('tree_content').show
    else
      @mgr.get_pane('tree_content').hide
    end
    @mgr.update
  end
  
  def toggleSaved(saved)
    cur_txt = @notebook.get_page_text(@notebook.get_selection)
    if(saved)
      cur_txt = cur_txt.split(" ").first
    else
      cur_txt += " *"
    end
    @notebook.set_page_text(@notebook.get_selection, cur_txt)
  end

onToggleBrowser will keep track of whether the left pane is visible or not and toggle it. toggleSaved will display an asterisk after the file name of each file that has been changed since last save, most of this logic is handled in each Scintilla and we will get to that in a subsequent article.

def displayTextEntry(dialog_text, headline)
    dialog = TextEntryDialog.new(self, dialog_text, headline, "", OK | CANCEL)
    return dialog.get_value() if dialog.show_modal() == ID_OK
  end
  
  def onSearchDocument
    return unless @sci
    str = self.displayTextEntry("", "Find in Document")
    lines = @sci.inLines(str)
    unless lines.empty?
      @treebook.add_page(self.create_tree(@project_tree, :file_jumper), str + " in " + @project_tree.getName(@sci.file_path), true)
      @cur_tree.buildFindDoc(lines, @notebook.get_selection)
      @mgr.update
    end
  end
  
  def onSearchProject
    return unless @sci
    str = self.displayTextEntry("", "Find in Project")
    files = @project_tree.inFiles(str, @sci.proj_id)
    unless files.empty?
      @treebook.add_page(self.create_tree(@project_tree, :proj_jumper), str + " in " + @project_tree.get_item_text(@sci.proj_id), true)
      @cur_tree.buildFindProj(files, @sci.proj_id)
      @mgr.update
    end
  end

Our two search functions. We pass the project tree to each new TreeCtrl like I said above.

def on_notebook_page_close(event)
    if @notebook == event.get_event_object
      unless @notebook.get_page(event.get_selection).saved
        msg = "Close anyway?"
        dlg = Wx::MessageDialog.new(self, msg, "Unsaved Document", Wx::YES_NO)
        if dlg.show_modal != Wx::ID_YES
          event.veto
        else
          event.allow
        end
      end
    end
  end

Here we check if the document has been saved or not, if not we ask the user if he wants to close it or not, if not we abort. Note also that we have to make sure that it is the right notebook that generated the event, the above code will make no sense when we close tabs in the left notebook.

And there it is, the main entry point and loop, not really exciting but I promise you, the next article is a lot sexier because then we discuss the code for the ProjTree.

Related Posts

Tags: , ,