# Purpose: Setup and initialize the core gui interfaces of the editpane
#
# $Id: editpane.rb,v 1.30 2004/10/05 19:45:37 ljulliar Exp $
#
# Authors:  Curt Hibbs <curt@hibbs.com>
#           Laurent Julliard <laurent AT moldus DOT org>
#           Richard Kilmer <rich AT infoether DOT com>
# Contributors: 
#
# This file is part of the FreeRIDE project
#
# This application is free software; you can redistribute it and/or
# modify it under the terms of the Ruby license defined in the
# COPYING file.
#
# Copyright (c) 2001 Curt Hibbs. All rights reserved.
# Copyright (c) 2002-2003 Laurent Julliard. All rights reserved.
# Copyright (c) 2002-2003 Rich Kilmer. All rights reserved.
#

require 'fox'
require 'fox/responder'
require 'rubyide_fox_gui/fxscintilla/scintilla'
require 'rubyide_fox_gui/editpane_configurator'

module FreeRIDE
  module FoxRenderer
    
    include Fox

    ##
    # This is the module that renders ediutpanes using
    # FXScintilla.
    #
    class EditPane
      include Fox
      ICON_PATH = "/system/ui/icons/EditPane"
      
      extend FreeBASE::StandardPlugin
      
      def EditPane.start(plugin)
        edit_book_slot = plugin["/system/ui/fox/editBook"]
        
        component_slot = plugin["/system/ui/components/EditPane"]

        # Subscribe to the editpane slot to render any newly created edit pane
        component_slot.subscribe do |event, slot|
          if (event == :notify_slot_add && slot.parent == component_slot)
            if (edit_book_slot.data == nil) 
              # create (only once) a TabBook in which all editor windows will appear 
              contentFrame = plugin["/system/ui/fox/contentFrame"].data
              tb = FXTabBook.new(contentFrame, nil, 0,LAYOUT_FILL_X|LAYOUT_FILL_Y)
              tb.create
              edit_book_slot.data = tb
              # catch selected tabitems on the tabbook (scn is the panel number 0,1,2...)
              tb.connect(SEL_COMMAND) do |sender, sel, scn|
                editpane_slot = tb.childAtIndex(2*scn).userData
                editpane_slot.manager.make_current
              end
            end
            Renderer.new(plugin, slot)
          end
        end

        # If a new icon is invoked through its slot then autoload the icon
        plugin[ICON_PATH].subscribe do |event, slot|
          if event == :notify_slot_add
            app = slot['/system/ui/fox/FXApp'].data
            path = "#{plugin.plugin_configuration.full_base_path}/icons/#{slot.name}.png"
            plugin.log_info << "iconpath : #{path}"
            if FileTest.exist?(path)
              slot.data = FXPNGIcon.new(app, File.open(path, "rb").read)
              slot.data.create
            end
          end
        end

        # Create the Goto Line EditPane Service Dialog box
        cmd_mgr = plugin["/system/ui/commands"].manager
        cmdg = cmd_mgr.add("EditPane/GotoLine", "Line...") do |cmd_slot|
          GotoLineDialog.new(plugin)
        end
        plugin["/system/ui/keys"].manager.bind("EditPane/GotoLine", :ctrl, :G)

        # Create the Find EditPane Service Dialog box
        cmdf = cmd_mgr.add("EditPane/Find", "&Find...") do |cmd_slot|
          sd = FindDialog.new(plugin)
          sd.execute
        end
        plugin["/system/ui/keys"].manager.bind("EditPane/Find", :ctrl, :F)

        # Create the Find/Replace EditPane Service Dialog box
        cmdr = cmd_mgr.add("EditPane/Replace", "&Replace...") do |cmd_slot|
          rd = ReplaceDialog.new(plugin)
          rd.execute
        end
        plugin["/system/ui/keys"].manager.bind("EditPane/Replace", :ctrl, :R)

        # Create the Find Next menu item
        cmdfn = cmd_mgr.add("EditPane/FindNext", "Find &next") do |cmd_slot|
          fd = FindDialog.new(plugin)
          fd.onCmdNext(nil,nil,nil)
        end
        plugin["/system/ui/keys"].manager.bind("EditPane/FindNext", :F3)
       
        # Create the Find Prev menu item
        cmdfp = cmd_mgr.add("EditPane/FindPrev", "Find pre&vious") do |cmd_slot|
          fd = FindDialog.new(plugin)
          fd.onCmdPrev(nil,nil,nil)
        end
        plugin["/system/ui/keys"].manager.bind("EditPane/FindPrev", :shift, :F3)
       
        # Command availability subject to same behavior for all three
        [cmdg,cmdf,cmdr,cmdfn,cmdfp].each do |cmd|
          cmd.availability = plugin['/system/ui/current'].has_child?('EditPane')
          cmd.manage_availability do |command|
            plugin['/system/ui/current'].subscribe do |event, slot|
              if slot.name=="EditPane"
                case event
                when :notify_slot_link
                  command.availability=true
                when :notify_slot_unlink
                  command.availability=false
                end
              end
            end
          end
        end
      
        # Insert the "goto line..." menu item in the Goto menu
        gotomenu = plugin["/system/ui/components/MenuPane/Goto_menu"].manager
        gotomenu.add_command("EditPane/GotoLine")

        # Insert the "Find..." and "Replace..." menu item in the Search menu
        searchmenu = plugin["/system/ui/components/MenuPane/Search_menu"].manager
        searchmenu.add_command("EditPane/Find")
        searchmenu.add_command("EditPane/Replace")
        searchmenu.add_command("EditPane/FindNext")
        searchmenu.add_command("EditPane/FindPrev")

	# Initialize configurator UI for the editor - Must be done
	# before any editpane is created
	EditPaneConfiguratorRenderer.new(plugin)
      
        # Restore files edited in the previous session
        component_slot.each_slot do |slot| 
          slot.notify(:notify_slot_add)
          slot.manager.load_file(slot.data)
          slot.notify(:notify_data_set)
          slot.manager.make_current
        end

	# Also load any file appearing on the command line
	ARGV.each do |filename|
	  # see if it has already been loaded
	  loaded = false
	  plugin['/system/ui/components/EditPane'].each_slot do |ep_slot|
	    if ep_slot.data == filename
	      loaded = true
	      break
	    end
	  end
	  
	  unless loaded
	    ep_slot = plugin['/system/ui/components/EditPane'].manager.add
	    ep_slot.manager.load_file(filename)
	  end
	end

        # Make the first editpane the current one
        component_slot.each_slot do |slot| 
          slot.manager.make_current
          break
        end

        # Now only is this plugin running
        plugin.transition(FreeBASE::RUNNING)
      end
      
      class Renderer
        include Fox
        
        attr_reader :plugin
        
        def initialize(plugin, slot)
          @plugin = plugin
          @slot = slot
          @app = plugin['/system/ui/fox/FXApp'].data
          @icons = plugin[ICON_PATH]
          @edit_book = plugin["/system/ui/fox/editBook"].data
          @tab = FXTabItem.new(@edit_book, slot.data, nil, TAB_TOP_NORMAL)
          @tab.iconPosition = ICON_AFTER_TEXT
          @frame = FXHorizontalFrame.new(@edit_book, FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y)
          @scintilla = FXScintilla.new(@frame, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
          @tab.create
          @tab.connect(SEL_FOCUSIN) {@scintilla.setFocus}
          @frame.create
          @scintilla.create
          # attach the slot to the 2 tabitem widget and the renderer to the
          # scintilla controller s so that they both know to which higher
          # level object they belong to
          @tab.userData = @slot
          @scintilla.userData = self
          @controller = ScintillaController.new(@scintilla)
          @scintilla.connect(SEL_COMMAND) do |sender, sel, scn|
            @controller.handle_notification(scn.nmhdr.idFrom, scn.nmhdr.code, scn)
          end
          @controller.setup
          setup_actions

          slot.subscribe do |event, slot|
            update(event) if ((event == :refresh) || (event == :notify_data_set))
          end

	  # Apply user-defined editor preferences
	  plugin['/plugins/rubyide_fox_gui-editpane/configurator'].manager.apply_all_config(@slot)

          @plugin.log_info << "EditPane created"
        end
        
        def update(event)
          if (event == :notify_data_set)
            # the data slot holds the file name so update the tabItem 
            if (@slot.data) 
              @tab.text = File.basename(@slot.data)
            else
              @tab.text = 'Untitled'
            end
          end
        end
        
        def setup_actions
          bind_action("filename", :filename)
          bind_action("load_file", :load_file)
          bind_action("make_current", :make_current)
          bind_action("close", :close)
          bind_action("save", :save)
          bind_action("neighbor", :neighbor)
          bind_action("modified", :modified?)
          bind_action("undo", :undo)
          bind_action("redo", :redo_cmd)
          bind_action("cut", :cut)
          bind_action("copy", :copy)
          bind_action("paste", :paste)
          bind_action("show_errorline", :show_errorline)
          bind_action("clear_errorline", :clear_errorline)
          bind_action("show_debugline", :show_debugline)
          bind_action("clear_debugline", :clear_debugline)
          bind_action("get_cursor_line", :cursor_line)
          bind_action("set_cursor_line", :set_cursor_line)
          bind_action("get_text", :get_text)
          bind_action("get_text_length", :text_length)
          bind_action("get_ext_object", :get_ext_object)
          bind_action("is_whitespace_visible", :is_whitespace_visible?)
          bind_action("whitespace_visible", :whitespace_visible=)
          bind_action("is_eol_visible", :is_eol_visible?)
          bind_action("eol_visible", :eol_visible=)
          bind_action("are_linenumbers_visible", :are_linenumbers_visible?)
          bind_action("linenumbers_visible", :linenumbers_visible=)
          bind_action("are_indentation_guides_visible", :are_indentation_guides_visible?)
          bind_action("indentation_guides_visible", :indentation_guides_visible=)
          bind_action("set_caret_period", :caret_period=)
          bind_action("get_caret_period", :caret_period)
          bind_action("set_wrap_mode", :wrap_mode=)
          bind_action("get_wrap_mode", :wrap_mode)
          bind_action("set_code_folding", :code_folding=)
          bind_action("get_code_folding", :code_folding)
          bind_action("get_editor_font", :editor_font=)
          bind_action("set_editor_font", :editor_font)
          bind_action("set_style", :set_style)
          bind_action("set_style_clear_all", :set_style_clear_all)
        end
        
        def bind_action(name, meth)
          @slot["actions/#{name}"].set_proc method(meth)
        end
        
        ### Commands ###
        
        def get_ext_object
          @controller.model
        end
        
        def filename
          @slot.manager.filename
        end
        
        def load_file(filename, breakpoints=nil)
          loaded = false
          begin
            @app.beginWaitCursor()
            begin
              @controller.open(filename)
            rescue
              cmd_mgr = @plugin["/system/ui/commands"].manager
              cmd_mgr.command('App/Services/MessageBox').invoke("File Load Error!",$!.to_s.wrap(60))
            else
              if breakpoints
                breakpoints.each {|line| @controller.toggle_breakpoint(line-1, false)}
              end
              loaded = true
            end
          ensure
            @app.endWaitCursor()
          end
          return loaded
        end
        
        def make_current
          index = @edit_book.indexOfChild(@tab)
          @edit_book.setCurrent(index/2)
          @edit_book.childAtIndex(index+1).setFocus()
          @scintilla.setFocus()
        end
        
        def close
          # FIXME? : how to delete an item in the tabbook? I'm not sure
          # for the moment I remove child 2*N+1 first (the scintilla pane) and then
          # the 2*N (the tabitem) second. It works.
          @tab.hide()
          @frame.removeChild(@scintilla)
          @edit_book.removeChild(@frame)
          @edit_book.removeChild(@tab)
          @tab = nil
          @frame = nil
          @tab = nil
        end
        
        def save(filename)
          begin
            @app.beginWaitCursor()
            begin
              @controller.save(filename)
            rescue
              cmd_mgr = @plugin["/system/ui/commands"].manager
              cmd_mgr.command('App/Services/MessageBox').invoke("File Save Error!",$!.to_s.wrap(60))
            end
          ensure
            @app.endWaitCursor()
          end
        end
        
        def neighbor
          tab_index = @edit_book.indexOfChild(@tab)
          if (tab_index == 0)
            if @edit_book.numChildren > 2
              tab_index += 2
            else
              return nil
            end
          else
            tab_index -= 2
          end
          @edit_book.childAtIndex(tab_index).userData
        end
        
        def modified?
          @controller.modified?
        end
        
        def undo
          @controller.undo
        end
        
        def redo_cmd
          @controller.redo
        end
        
        def cut
          @controller.cut
        end
        
        def is_eol_visible?
          @controller.is_eol_visible?
        end
        
        def eol_visible=(value)
          @controller.eol_visible = value
        end
        
        def is_whitespace_visible?
          @controller.is_whitespace_visible?
        end
        
        def whitespace_visible=(value)
          @controller.whitespace_visible=value
        end
        
        def are_linenumbers_visible?
          @controller.are_linenumbers_visible?
        end
        
        def linenumbers_visible=(value)
          @controller.linenumbers_visible=value
        end
      
        def are_indentation_guides_visible?
          @controller.are_indentation_guides_visible?
        end
        
        def indentation_guides_visible=(value)
          @controller.indentation_guides_visible=value
        end   
     
        def is_eol_visible?
          @controller.is_eol_visible?
        end
        
        def caret_period
          @controller.caret_period
        end
     
        def caret_period=(msec)
          @controller.caret_period = msec
        end
     
        def wrap_mode
          @controller.wrap_mode
        end        

        def wrap_mode=(status)
          @controller.wrap_mode = status
        end
     
        def code_folding
          @controller.code_folding
        end        

        def code_folding=(status)
          @controller.code_folding = status
        end

        def editor_font
          @controller.editor_font
        end        

        def editor_font=(font)
          @controller.editor_font = font
        end

        def set_style(style_name, style)
          @controller.set_style(style_name,style)
        end

        def set_style_clear_all
          @controller.set_style_clear_all
        end
           
        def copy
          @controller.copy
        end
        
        def paste
          @controller.paste
        end
        
        def show_errorline(line)
          @controller.show_errorline(line)
        end
        
        def clear_errorline
          @controller.show_errorline(nil)
        end

        def show_debugline(line)
          @controller.show_debugline(line)
        end
        
        def clear_debugline
          @controller.show_debugline(nil)
        end
        
        def cursor_line
          @controller.cursor_line+1
        end
        
        def set_cursor_line(line)
          @controller.cursor_line = line-1
        end

        def get_text
          @controller.get_text
        end

        def text_length
          @controller.text_length
        end
        
        ##
        # called from the scintilla controller whenever the modified
        # status flag is updated
        #
        #  
        #  Return:: [Boolean] true if it debugger paused
        #
        def modified=(flag)
          if flag
            icon = @icons['modified'].data
            @tab.icon = icon
          else
            @tab.setIcon(nil)
          end
        end
        
        ##
        # add a breakpoint on the line
        # called from the scintilla controller
        #
        def add_breakpoint(line)
          @slot.manager.add_breakpoint(line)
        end
        
        ##
        # delete a breakpoint on the line
        # called from the scintilla controller
        #
        def delete_breakpoint(line)
          @slot.manager.delete_breakpoint(line) 
        end
        
      end  # class Renderer
      

      class GotoLineDialog < FXDialogBox

        include Fox
        def initialize(plugin)
          @plugin = plugin
          owner = plugin["/system/ui/fox/FXMainWindow"].data
          
          # Invoke base class initialize function first
          super(owner, "Goto Line", DECOR_TITLE|DECOR_BORDER|DECOR_CLOSE)
          h_frm = FXHorizontalFrame.new(self, LAYOUT_FILL_X)
          FXLabel.new(h_frm, "Line: ", nil, JUSTIFY_LEFT|LAYOUT_CENTER_Y)
          line_tf = FXTextField.new(h_frm, 12, nil, 0, (FRAME_SUNKEN|
                  LAYOUT_FILL_X|LAYOUT_CENTER_Y|LAYOUT_FILL_COLUMN))
          line_tf.connect(SEL_COMMAND, method(:onCmdFetchLine))
          line_tf.setFocus
          self.connect(SEL_CLOSE) { self.destroy }
          self.create
          self.show(PLACEMENT_OWNER)
        end
        
        def onCmdFetchLine(sender, sel, ptr)
          line = sender.text.to_i
          ep_slot = @plugin['/system/ui/current/EditPane']
          ep_slot['actions/set_cursor_line'].invoke(line) if line > 0
          # return focus to the edit pane when the dialog box closes
          ep_slot['actions/make_current'].invoke()
          self.destroy
        end

      end # class GotoLineDialog


      class ReplaceDialog < FXDialogBox

        include Fox
        include Responder

        attr_accessor :accept, :every, :inselection, :replacelabel, :replacebox

        HORZ_PAD    = 10
        VERT_PAD    = 2

        ID_REPLACE,
        ID_NEXT_MATCH,
        ID_PREV_MATCH,
        ID_SEARCH_TEXT,
        ID_REPLACE_TEXT,
        ID_REPLACE_ALL,
        ID_REPLACE_INSEL,
        ID_MODE_WHOLEWORD,
        ID_MODE_WRAP,
        ID_MODE_MATCHCASE,
        ID_MODE_BACKSLASH,
        ID_MODE_WORDSTART,
        ID_MODE_REGEXP,
        ID_MODE_DIR,
        ID_LAST = enum(FXDialogBox::ID_LAST, 32)
        
        # Search mode flags
        SEARCH_WHOLEWORD   = 0x1
        SEARCH_WRAP        = 0x2
        SEARCH_MATCHCASE   = 0x4
        SEARCH_BACKSLASH   = 0x8
        SEARCH_REGEXP      = 0x10
        SEARCH_REGEXPPOSIX = 0x20
        SEARCH_BACKWARD    = 0x40
        SEARCH_WORDSTART   = 0x80

	# Max number of elements in sear/replace combobox history
	HISTORY_SIZE = 10

        def initialize(plugin)
          @plugin = plugin
          @app = plugin["/system/ui/fox/FXApp"].data
          owner = plugin["/system/ui/fox/FXMainWindow"].data
          ic = nil
          @havefound = false

          FXMAPFUNC(SEL_COMMAND,   ID_REPLACE,           :onCmdReplaceOnce)
          FXMAPFUNC(SEL_COMMAND,   ID_REPLACE_TEXT,      :onCmdSearchText)
          FXMAPFUNC(SEL_COMMAND,   ID_SEARCH_TEXT,       :onCmdSearchText)
          FXMAPFUNC(SEL_COMMAND,   ID_REPLACE_ALL,       :onCmdReplaceAll)
          FXMAPFUNC(SEL_COMMAND,   ID_REPLACE_INSEL,     :onCmdReplaceAll)
          FXMAPFUNC(SEL_COMMAND,   ID_PREV_MATCH,        :onCmdPrev)
          FXMAPFUNC(SEL_COMMAND,   ID_NEXT_MATCH,        :onCmdNext)
          FXMAPFUNC(SEL_UPDATE,    ID_MODE_MATCHCASE,    :onUpdMatchCase)
          FXMAPFUNC(SEL_COMMAND,   ID_MODE_MATCHCASE,    :onCmdMatchCase)
          FXMAPFUNC(SEL_UPDATE,    ID_MODE_WHOLEWORD,    :onUpdWholeWord)
          FXMAPFUNC(SEL_COMMAND,   ID_MODE_WHOLEWORD,    :onCmdWholeWord)
          FXMAPFUNC(SEL_UPDATE,    ID_MODE_WRAP,         :onUpdWrap)
          FXMAPFUNC(SEL_COMMAND,   ID_MODE_WRAP,         :onCmdWrap)
          FXMAPFUNC(SEL_UPDATE,    ID_MODE_REGEXP,       :onUpdRegexp)
          FXMAPFUNC(SEL_COMMAND,   ID_MODE_REGEXP,       :onCmdRegexp)
          FXMAPFUNC(SEL_UPDATE,    ID_MODE_WORDSTART,    :onUpdWordStart)
          FXMAPFUNC(SEL_COMMAND,   ID_MODE_WORDSTART,    :onCmdWordStart)
          FXMAPFUNC(SEL_COMMAND,   ID_CANCEL,            :onCmdCancel)


          # Invoke base class initialize function first
          super(owner, getDialogTitle, DECOR_TITLE|DECOR_BORDER|DECOR_CLOSE)
          buttons = FXHorizontalFrame.new(self, LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|PACK_UNIFORM_WIDTH|PACK_UNIFORM_HEIGHT)
          cancel = FXButton.new(buttons,"&Cancel",nil,self,ID_CANCEL,BUTTON_DEFAULT|FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_Y|LAYOUT_RIGHT,0,0,0,0,HORZ_PAD,HORZ_PAD,VERT_PAD,VERT_PAD);
          inselection = FXButton.new(buttons,"In &Selection",nil,self,ID_REPLACE_INSEL,BUTTON_DEFAULT|FRAME_RAISED|FRAME_THICK|LAYOUT_CENTER_Y|LAYOUT_RIGHT,
                     0,0,0,0,6,6,VERT_PAD,VERT_PAD);
          @inselection = inselection
          every = FXButton.new(buttons,"Replace &All",nil,self,ID_REPLACE_ALL,BUTTON_DEFAULT|FRAME_RAISED|FRAME_THICK|LAYOUT_CENTER_Y|LAYOUT_RIGHT,
                     0,0,0,0,6,6,VERT_PAD,VERT_PAD);
          @every = every
          accept = FXButton.new(buttons,"&Replace",nil,self,ID_REPLACE,BUTTON_INITIAL|BUTTON_DEFAULT|FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_Y|LAYOUT_RIGHT,
                            0,0,0,0,HORZ_PAD,HORZ_PAD,VERT_PAD,VERT_PAD);
          @accept = accept
           pair = FXHorizontalFrame.new(buttons,LAYOUT_FILL_Y|LAYOUT_RIGHT, 0,0,0,0, 0,0,0,0);
          searchlast = FXArrowButton.new(pair,self,ID_PREV_MATCH,ARROW_LEFT|FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_Y, 0,0,0,0,HORZ_PAD,HORZ_PAD,VERT_PAD,VERT_PAD);
          searchnext = FXArrowButton.new(pair,self,ID_NEXT_MATCH,ARROW_RIGHT|FRAME_RAISED|FRAME_THICK|LAYOUT_FILL_Y, 0,0,0,0,HORZ_PAD,HORZ_PAD,VERT_PAD,VERT_PAD);
          FXHorizontalSeparator.new(self,SEPARATOR_GROOVE|LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X);

          toppart = FXHorizontalFrame.new(self,LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|LAYOUT_CENTER_Y, 0,0,0,0, 0,0,0,0, 10,10);
          FXLabel.new(toppart,nil,ic,ICON_BEFORE_TEXT|JUSTIFY_CENTER_X|JUSTIFY_CENTER_Y|LAYOUT_FILL_Y|LAYOUT_FILL_X)

          entry = FXVerticalFrame.new(toppart,LAYOUT_FILL_X|LAYOUT_CENTER_Y, 0,0,0,0, 0,0,0,0);
          searchlabel = FXLabel.new(entry,"S&earch for:",nil,JUSTIFY_LEFT|ICON_BEFORE_TEXT|LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X);

          searchbox = FXComboBox.new(entry,5,5,self,ID_SEARCH_TEXT,FRAME_SUNKEN|FRAME_THICK|LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
          @searchbox = searchbox
          replacelabel = FXLabel.new(entry,"Replace &with:",nil,LAYOUT_LEFT);
          @replacelabel = replacelabel
          replacebox = FXComboBox.new(entry,5,5,self,ID_REPLACE_TEXT,FRAME_SUNKEN|FRAME_THICK|LAYOUT_SIDE_TOP|LAYOUT_FILL_X)
          @replacebox = replacebox

          options1 = FXMatrix.new(entry,2, FRAME_NONE|MATRIX_BY_COLUMNS|PACK_UNIFORM_WIDTH|LAYOUT_FILL_Y,0,0,0,0,0,0,0,0,0,0)
          FXCheckButton.new(options1,"Match whole word &only",self,ID_MODE_WHOLEWORD,ICON_BEFORE_TEXT|JUSTIFY_LEFT)
          FXCheckButton.new(options1,"&Wrap around",self,ID_MODE_WRAP,ICON_BEFORE_TEXT|JUSTIFY_LEFT)
          FXCheckButton.new(options1,"&Match case",self,ID_MODE_MATCHCASE,ICON_BEFORE_TEXT|JUSTIFY_LEFT)
          FXCheckButton.new(options1,"Regular e&xpression",self,ID_MODE_REGEXP,ICON_BEFORE_TEXT|JUSTIFY_LEFT)
          FXCheckButton.new(options1,"Match at word start",self,ID_MODE_WORDSTART,ICON_BEFORE_TEXT|JUSTIFY_LEFT)

          # Some hot keys Ctrl-f for forward search
          # Ctrl-b for backward search
          # Ctrl-r for find forward and replace 
          searchlast.addHotKey(MKUINT(KEY_b,CONTROLMASK))
          searchnext.addHotKey(MKUINT(KEY_f,CONTROLMASK))
          accept.addHotKey(MKUINT(KEY_r,CONTROLMASK))

          # restore search and replace text field state and history
          # as well as last search mode
          restoreSettings()

        end
        
        def getDialogTitle
          "Replace Text..."
        end
        
        def execute()
          self.create
          #@searchbox.setFocus
	  # Mimic a TAB key pressed because the setFocus doesn't work
	  # on FXComboBox (bug!)
	  fxevt = FXEvent.new()
	  fxevt.code = Fox::KEY_Tab
	  fxevt.type = Fox::SEL_KEYPRESS
	  @searchbox.handle(self,MKUINT(0, SEL_KEYPRESS),fxevt)
          x = @plugin.properties["findsearch/location/x"]
          y = @plugin.properties["findsearch/location/y"]
          x ||= 1
          y ||= 1

          position(x,y,self.width,self.height)
          if x==1
            show(PLACEMENT_OWNER)
          else
            show
          end
          @app.runModalFor(self)
        end

        def model()
          ep_slot = @plugin['/system/ui/current/EditPane']
          ep_slot['actions/get_ext_object'].invoke()
        end

        ##
        # Prompt a message in the status bar
        #
        def status(msg)
          @plugin['/system/ui/current/StatusBar/actions/prompt'].invoke(msg)
        end

        def replaceText()
          @replacebox.getText()
        end

        def replaceText=(text)
          @replacebox.setText(text)
        end


        def searchText()
          @searchbox.getText()
        end

        def searchText=(text)
          @searchbox.setText(text)
        end

        def searchMode()
          @searchmode
        end

        def searchMode=(mode)
          @searchmode = mode
          @plugin.properties['findreplace/search_mode'] = mode
        end

        def searchBackward? ()
          (self.searchMode&SEARCH_BACKWARD) != 0
        end

        def searchWrap? ()
          (self.searchMode&SEARCH_WRAP) != 0
        end

        def searchWholeWord? ()
          (self.searchMode&SEARCH_WHOLEWORD) != 0
        end

        def searchMatchCase? ()
          (self.searchMode&SEARCH_MATCHCASE) != 0
        end

        def searchRegExp? ()
          (self.searchMode&SEARCH_REGEXP) != 0
        end

        def searchRegExpPosix? ()
          (self.searchMode&SEARCH_REGEXPPOSIX) != 0
        end

        def searchMatchWordStart? ()
          (self.searchMode&SEARCH_WORDSTART) != 0
        end

        def onCmdReplaceOnce(sender, sel, ptr)
          if (@havefound) 
            selstart = model.selection_start
            selend = model.selection_end

            model.target_start = selstart
            model.target_end = selend
            if (self.searchRegExp?)
              lenreplaced = model.replace_target_re(self.replaceText())
            else
              lenreplaced = model.replace_target(self.replaceText())
            end
            model.set_sel(selstart+lenreplaced, selstart)
            @havefound = false
          end
          onCmdNext(sender, sel, ptr)
        end

        def onCmdReplaceAll(sender, sel, ptr)

          if searchText().empty? 
            return 1 
          end
          findlen = searchText().size
          replacelen = replaceText().size

          selstart = startpos = model.selection_start
          selend = endpos = model.selection_end
          if (SELID(sel) == ID_REPLACE_INSEL)
            return 1 if selstart == selend
          else
            endpos = model.get_length
            # if wrap mode then replace from beginning to end
            # else from caret to end
            if (self.searchWrap?)
              startpos = 0
            end
          end

          searchflags = (searchWholeWord?   ? Scintilla::SCFIND_WHOLEWORD : 0) |
                        (searchMatchCase?   ? Scintilla::SCFIND_MATCHCASE : 0) |
                        (searchRegExp?      ? Scintilla::SCFIND_REGEXP    : 0) |
                        (searchMatchWordStart? ? Scintilla::SCFIND_WORDSTART : 0)

          model.target_start = startpos
          model.target_end = endpos
          model.search_flags = searchflags
          posfind = model.search_in_target(searchText())

          if ((findlen == 1) && self.searchRegExp? && (self.searchText()[0..0] == '^'))
            # Special case for replace all start of line so it hits the first line
            posfind = startpos;
            model.target_start = startpos
            model.target_end = startpos
          end

          if ((posfind != -1) && (posfind <= endpos))
            lastmatch = posfind
            occurences = 0
            model.begin_undo_action()

            while (posfind != -1)
              lentarget = model.target_end - model.target_start
              movepasteol = 0
              if (lentarget <= 0)
                nextchar = model.char_at(model.target_end)
                if (nextchar=="\r" || nextchar=="\n")
                  # FIXME? shall we test EOL mode and add 2 if it is
                  # SC_EOL_CRLF
                  movepasteol = 1
                end
              end

              lenreplaced = replacelen
              if (self.searchRegExp?)
                lenreplaced = model.replace_target_re(self.replaceText())
              else
                lenreplaced = model.replace_target(self.replaceText())
              end

              # modify end position of the target to reflect the last change
              endpos += lenreplaced - lentarget
              lastmatch = posfind + lenreplaced + movepasteol
              if (lastmatch >= endpos)
                # run off the endof the document with an empty match
                posfind = -1
              else
                model.target_start = lastmatch
                model.target_end = endpos
                posfind = model.search_in_target(searchText())
              end
              occurences = occurences.succ
            end #while

            if (SELID(sel) == ID_REPLACE_INSEL)
              model.set_sel(startpos,endpos)
            else
              model.set_sel(lastmatch,lastmatch)
            end
            model.end_undo_action()
            status("Replaced #{occurences} occurence"+(occurences>1 ? 's':''))

          end # if
          return 1
        end

        def onCmdPrev(sender, sel, ptr)
          self.searchMode |= SEARCH_BACKWARD
          onCmdNext(sender, sel, ptr)
        end

        def onCmdNext(sender, sel, ptr)
          @havefound = false
          if searchText().empty? 
            self.searchMode &= ~SEARCH_BACKWARD
            return 1 
          end

          selstart = model.selection_start
          selend = model.selection_end
          if (searchBackward?) 
            startpos = selstart - 1;
            endpos = 0;
          else
            startpos = selend
            endpos = model.length
          end

          searchflags = (searchWholeWord?   ? Scintilla::SCFIND_WHOLEWORD : 0) |
                        (searchMatchCase?   ? Scintilla::SCFIND_MATCHCASE : 0) |
                        (searchRegExp?      ? Scintilla::SCFIND_REGEXP    : 0) |
                        (searchMatchWordStart? ? Scintilla::SCFIND_WORDSTART : 0)

          model.target_start = startpos
          model.target_end = endpos
          model.search_flags = searchflags
          posfind = model.search_in_target(searchText())

          if (posfind == -1 && searchWrap?)
            # Failed to find in indicated direction
            # so search from the beginning (forward) or from the end (reverse)
            if ( searchBackward? )
              startpos = model.length
              endpos = 0
            else
              startpos = 0
              endpos =  model.length
            end
            model.target_start = startpos
            model.target_end = endpos
            posfind = model.search_in_target(searchText())          
            status("Failing search: #{searchText()}. About to wrap")
          end
          if (posfind == -1)
            status("Failing search: #{searchText()}")
          else
            @havefound = true
            start = model.target_start()
            finish = model.target_end()
            model.set_sel(start,finish)
            status("Found '#{searchText()}' at line #{model.line_from_position(start)}, column #{model.get_column(start)}")
          end
          self.searchMode &= ~SEARCH_BACKWARD
          @app.stopModal(self)
          return 1
        end

        def restoreSettings()

          @search_history = @plugin.properties['findreplace/search_hist'] || Array.new
          @replace_history = @plugin.properties['findreplace/replace_hist'] || Array.new
	  @search_history.slice!(-HISTORY_SIZE..-1) if @search_history.size > HISTORY_SIZE
	  @replace_history.slice!(-HISTORY_SIZE..-1) if @replace_history.size > HISTORY_SIZE

          @last_search = @plugin.properties['findreplace/last_search']
          @last_replace = @plugin.properties['findreplace/last_replace']
          self.searchMode = @plugin.properties['findreplace/search_mode'] || SEARCH_WRAP
          @search_history.each  { |elt|  @searchbox.prependItem(elt) }
          @replace_history.each { |elt|  @replacebox.prependItem(elt) }

          unless @search_history.empty?
            self.searchText = @last_search
          end
          unless @replace_history.empty?
            self.replaceText = @last_replace
          end

        end

        def appendHistory(search_text, replace_text)
          @last_search = elt = searchText()
          unless @search_history.include?(elt)
	    @search_history << elt 
	    @search_history.shift if @search_history.size > HISTORY_SIZE
	    @plugin.properties.auto_save = false
	    @plugin.properties['findreplace/search_hist'] = @search_history
	    @plugin.properties['findreplace/last_search'] = @last_search
	  end

          @last_replace = elt = replaceText()
	  unless @replace_history.include?(elt)
	    @replace_history << elt
	    @replace_history.shift if @replace_history.size > HISTORY_SIZE
	    @plugin.properties['findreplace/replace_hist'] = @replace_history
	    @plugin.properties['findreplace/last_replace'] = @last_replace
	  end
        end

        def onCmdSearchText(sender, sel, ptr)
	  return 0 if sender.text.empty?

	  # let's keep our own history
          appendHistory(searchText(), replaceText())

	  # only insert the new element in the combobox 
	  # list if it doesn't yet exist
	  found = false
	  text = sender.text
	  num = sender.getNumItems
	  for idx in 0..num-1
	    if sender.getItemText(idx) == text
	      found = true
	      sender.setCurrentItem(idx)
	      break
	    end
	  end

	  # insert the new item if needed and only keep 
	  # the last HISTORY_SIZE elements
	  unless found
	    sender.prependItem(text)
	    sender.setCurrentItem(0)
	    sender.removeItem(num) if num == HISTORY_SIZE
	  end
	  # This mehtod is called whenever the return key is typed
	  # so also search for the next/prev occurence
	  (self.searchMode & SEARCH_BACKWARD) != 0 ? onCmdPrev(sender,sel,ptr) : onCmdNext(sender,sel,ptr)
          return 1
        end

        def onUpdMatchCase(sender, sel, ptr)
          if (self.searchMode&SEARCH_MATCHCASE != 0)
            sender.handle(self,MKUINT(ID_CHECK,SEL_COMMAND),nil)
          else
            sender.handle(self,MKUINT(ID_UNCHECK,SEL_COMMAND),nil)
          end     
          return 1
        end

        def onCmdMatchCase(sender, sel, ptr)
          self.searchMode ^= SEARCH_MATCHCASE
          return 1
        end

        def onUpdWholeWord(sender, sel, ptr)
          if (self.searchMode&SEARCH_WHOLEWORD != 0)
            sender.handle(self,MKUINT(ID_CHECK,SEL_COMMAND),nil)
          else
            sender.handle(self,MKUINT(ID_UNCHECK,SEL_COMMAND),nil)
          end
          return 1
        end

        def onCmdWholeWord(sender, sel, ptr)
          self.searchMode ^= SEARCH_WHOLEWORD
          return 1
        end

        def onUpdWrap(sender, sel, ptr)
          if (self.searchMode&SEARCH_WRAP != 0)
            sender.handle(self,MKUINT(ID_CHECK,SEL_COMMAND),nil)
          else
            sender.handle(self,MKUINT(ID_UNCHECK,SEL_COMMAND),nil)
          end     
          return 1
        end

        def onCmdWrap(sender, sel, ptr)
          self.searchMode ^= SEARCH_WRAP
          return 1
        end

        def onUpdRegexp(sender, sel, ptr)
          if (self.searchMode&SEARCH_REGEXP != 0)
            sender.handle(self,MKUINT(ID_CHECK,SEL_COMMAND),nil)
          else
            sender.handle(self,MKUINT(ID_UNCHECK,SEL_COMMAND),nil)
          end     
          return 1
        end

        def onCmdRegexp(sender, sel, ptr)
          self.searchMode ^= SEARCH_REGEXP
          return 1
        end

        def onUpdBackslash(sender, sel, ptr)
          if (self.searchMode&SEARCH_BACKSLASH != 0)
            sender.handle(self,MKUINT(ID_CHECK,SEL_COMMAND),nil)
          else
            sender.handle(self,MKUINT(ID_UNCHECK,SEL_COMMAND),nil)
          end     
          return 1
        end

        def onCmdBackslash(sender, sel, ptr)
          self.searchMode ^= SEARCH_BACKSLASH
          return 1
        end

        def onUpdWordStart(sender, sel, ptr)
          if (self.searchMode&SEARCH_WORDSTART != 0)
            sender.handle(self,MKUINT(ID_CHECK,SEL_COMMAND),nil)
          else
            sender.handle(self,MKUINT(ID_UNCHECK,SEL_COMMAND),nil)
          end     
          return 1
        end

        def onCmdWordStart(sender, sel, ptr)
          self.searchMode ^= SEARCH_WORDSTART
          return 1
        end

        def onCmdCancel(sender, sel, ptr)
          @plugin.properties["findsearch/location/x"] = self.x
          @plugin.properties["findsearch/location/y"] = self.y
          @app.stopModal(self)
          self.destroy
          return 1
        end

      end # class FindDialog

      ## 
      # The Search Dialog box is basically the same as the replace
      # dialog box minus some hidden fields
      #
      class FindDialog < ReplaceDialog
        
        def initialize(plugin)
          super(plugin)

	  # hide all controls that are for text replacement
          accept.hide
          every.hide
          replacelabel.hide
          replacebox.hide
          inselection.hide

	  # if there is a piece of text selected in the current edit pane
	  # then this is the text to search
	  ep_slot = @plugin['/system/ui/current/EditPane']
          model = ep_slot['actions/get_ext_object'].invoke()
	  text_selection = model.get_sel_text()
	  unless text_selection.empty?
	    self.searchText = text_selection
	  end

        end

        def getDialogTitle
          "Find Text..."
        end
        
      end # class FindDialog

    end # class EditPane

  end
end

