#!/usr/bin/ruby -w =begin /*************************************************************************** * Copyright (C) 2006, Paul Lutus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ =end # user-defined, platform specific viewers EDITOR='kwrite' VIEWER='kuickshow' require 'searchreplaceglobalui_ui.rb' require 'searchreplaceglobalhelp' require 'find' PROGRAM_VERSION = '1.4' =begin The Configuration class defines program values to be preserved in a configuration file. =end class Configuration attr_accessor :app_xpos attr_accessor :app_ypos attr_accessor :app_xsize attr_accessor :app_ysize attr_accessor :file_filter_list attr_accessor :search_for_list attr_accessor :replace_with_list attr_accessor :subdirs attr_accessor :global attr_accessor :caseSens attr_accessor :multiLine attr_accessor :reverse attr_accessor :changed_file_list attr_accessor :search_path_list def initialize @app_xpos = -1 @app_ypos = -1 @app_xsize = -1 @app_ysize = -1 @file_filter_list = nil @search_for_list = nil @replace_with_list = nil @changed_file_list = nil @search_path_list = nil @subdirs = true @global = true @caseSens = false @multiline = true @reverse = false end end =begin The ConfigurationHandler class reads and writes the program configuration file and populates a Configuration class instance =end class ConfigurationHandler attr_accessor :ini_path def initialize(conf,parent) @conf = conf @prog_name = parent.className() @conf_path = File.join(ENV["HOME"], "." + @prog_name) @ini_path = @conf_path + "/" + @prog_name + ".ini" Dir.mkdir(@conf_path) unless FileTest.exists?(@conf_path) end def write_config() file = File.new(@ini_path,"w") unless file.nil? @conf.instance_variables.sort.each { |x| xi = @conf.instance_variable_get(x) # escape strings if(xi.class == String) xi.gsub!(/\\/,"\\\\\\\\") xi.gsub!(/"/,"\\\"") xi = "\"#{xi}\"" end file.write("#{x}=#{xi}\n") } file.close() end end def read_config() if FileTest.exists?(@ini_path) file = File.new(@ini_path,"r") file.each { |line| @conf.instance_eval(line) } file.close() end end end # main class class SearchReplaceGlobal < SearchReplaceGlobalUI attr_accessor :ini_file def initialize(app,argv) super() @app = app @argv = argv setCaption(self.className + " " + PROGRAM_VERSION) @file_list = nil @changed_file_list = nil @config = Configuration.new @configHandler = ConfigurationHandler.new(@config,self) read_config() @ini_file = @configHandler.ini_path @help_engine = SearchReplaceGlobalHelp.new(self) @fileFilterComboBox.setFocus() end def beep @app.beep end def write_combo_box(widget,data) if(data) update_combo_box(widget,data.split("\t")) else widget.insertItem("") end end def read_config() @configHandler.read_config() if(@config.changed_file_list && @config.changed_file_list.strip.length > 0) @changed_file_list = @config.changed_file_list.split("\t") @changedTextEdit.setText(@changed_file_list.sort.join("\n")) end write_combo_box(@searchPathComboBox,@config.search_path_list) write_combo_box(@fileFilterComboBox,@config.file_filter_list) write_combo_box(@searchComboBox,@config.search_for_list) write_combo_box(@replaceComboBox,@config.replace_with_list) @subdirsCheckBox.setChecked(@config.subdirs) @globalCheckBox.setChecked(@config.global) @caseCheckBox.setChecked(@config.caseSens) @reverseCheckBox.setChecked(@config.reverse) @multiLineCheckBox.setChecked(@config.multiLine) if(@config.app_xpos != -1) move(@config.app_xpos,@config.app_ypos) resize(@config.app_xsize,@config.app_ysize) end end def read_combo_box(widget) array = update_combo_box(widget) return array.join("\t") end def write_config() @config.app_xsize = width() @config.app_ysize = height() @config.app_xpos = x() @config.app_ypos = y() @config.subdirs = @subdirsCheckBox.isChecked() @config.global = @globalCheckBox.isChecked() @config.caseSens = @caseCheckBox.isChecked() @config.multiLine = @multiLineCheckBox.isChecked() @config.reverse = @reverseCheckBox.isChecked() @config.changed_file_list = (@changed_file_list)?@changed_file_list.join("\t"):"" @config.search_path_list = read_combo_box(@searchPathComboBox) @config.file_filter_list = read_combo_box(@fileFilterComboBox) @config.search_for_list = read_combo_box(@searchComboBox) @config.replace_with_list = read_combo_box(@replaceComboBox) @configHandler.write_config() end # override default close() method def close(*x) if(Qt::MessageBox::question(self, self.className.to_s,"Okay to close application?",Qt::MessageBox::No,Qt::MessageBox::Yes) == Qt::MessageBox::Yes) write_config() @app.exit(0) end end def build_file_tree() update_all_combo_boxes() begin @file_list = [] path = @searchPathComboBox.text(0).strip path = (path.length == 0)?".":path search = @fileFilterComboBox.text(0).strip Find.find(path) do |item| unless(FileTest.directory?(item)) if(search.length == 0 || item =~ %r{#{search}}) @file_list << item end end end rescue Exception => err Qt::MessageBox::warning(self, self.className.to_s,"Error in file search:\n\n\"" + err.to_s + "\"") end end def build_tree() build_file_tree() @resultsTextEdit.setText(@file_list.sort.join("\n")) statusBar.message("Found #{@file_list.size} matching files.") end def unescape(s) shift = false; len = s.length; output = ""; i = 0; while(i < len) c = s[i,1] if(shift) case(c) when "t" then output += "\t" when "n" then output += "\n" when "r" then output += "\r" when "f" then output += "\f" when "a" then output += "\a" when "e" then output += "\e" when "b" then output += "\b" when "v" then output += "\v" when "0" then output += "\0" # handle case of /c(control letter) when "c" then if(i < len-1) i += 1 output += (s[i] & 0x1f).chr; end else output += "\\" + c end shift = false; else # not shifted if(c == "\\") shift = true; else output += c; end end i += 1 end return output end def build_search_regex() search = unescape(@searchComboBox.text(0)) options = 0 options |= Regexp::MULTILINE if @multiLineCheckBox.isChecked() options |= Regexp::IGNORECASE unless @caseCheckBox.isChecked() return Regexp.new(search,options) end def find_matches() build_file_tree() begin reversed = @reverseCheckBox.isChecked() results = [] @file_list.each do |path| data = File.read(path) regex = build_search_regex() n = data.scan(regex) outcome = n.size > 0 outcome = !outcome if reversed if(outcome) results << sprintf("[%6d] ",n.size) + path end end mod = (reversed)?"non-":"" # show highest occurrences near top of list, # but sort alphabetically within equal # numbers of occurrences sorted = results.sort { |a,b| if a[0 .. 8] == b[0 .. 8] a[8 .. -1] <=> b[8 .. -1] else b <=> a end } @resultsTextEdit.setText(sorted.join("\n")) statusBar.message("Found #{results.size} #{mod}matching files.") rescue Exception => err Qt::MessageBox::warning(self, self.className.to_s,"Error in content search:\n\n\"" + err.to_s + "\"") end end def search_replace() build_file_tree() begin reply = Qt::MessageBox::critical(self, self.className.to_s,"Warning: search & replace on " + @file_list.size.to_s + "\nfile(s). Proceed?","Yes","Rehearse","No",2) if(reply < 2) replace = unescape(@replaceComboBox.lineEdit.text()) @changed_file_list = [] build_tree() regex = build_search_regex() @file_list.each do |path| data = File.read(path) if(@globalCheckBox.isChecked()) result = data.gsub(regex,replace) else result = data.sub(regex,replace) end if(result != data) if(reply == 0) backup_path = path + "~" unless FileTest.exist?(backup_path) File.open(backup_path,"w") { |f| f.write(data) } end File.open(path,"w") { |f| f.write(result) } end @changed_file_list << path end end @changedTextEdit.setText(@changed_file_list.sort.join("\n")) s = (reply == 0)?"Changed":"Would have changed" statusBar.message(s + " #{@changed_file_list.size} files.") end rescue Exception => err Qt::MessageBox::warning(self, self.className.to_s,"Error in search & replace:\n\n\"" + err.to_s + "\"") end end def revert() if(@changed_file_list) original_changed = @changed_file_list.size recovered = [] lost = [] reply = Qt::MessageBox::critical(self, self.className.to_s,"Warning: attempt to restore " + @changed_file_list.size.to_s + "\nchanged file(s). Proceed?","Yes","No","Cancel",1) if(reply == 0) @changed_file_list.each do |path| backup_path = path + "~" if FileTest.exists?(backup_path) data = File.read(backup_path) File.open(path,"w") { |f| f.write(data) } recovered << path File.delete(backup_path) else lost << path end end @changed_file_list = lost @resultsTextEdit.setText(recovered.sort.join("\n")) @changedTextEdit.setText(@changed_file_list.sort.join("\n")) statusBar.message("Restored #{recovered.size} out of " + original_changed.to_s + " files from backups.") end end end def edit_file(k,widget) widget.setSelection(k[0],0,k[0],widget.paragraphLength(k[0])) path = widget.selectedText().chomp path.sub!(%r{\[.*?\]},"") if(path =~ /\.(jpg|jpeg|bmp|gif|png|xpm|cpt)$/i) system("#{VIEWER} #{path} &") else system("#{EDITOR} #{path} &") end end def choosePath() fd = Qt::FileDialog.new(@searchPathComboBox.currentText()) fd.setMode(Qt::FileDialog::DirectoryOnly) if(fd.exec() == Qt::Dialog::Accepted) path = fd.selectedFile(); @searchPathComboBox.insertItem(path,0) @searchPathComboBox.setCurrentItem(0) update_combo_box(@searchPathComboBox) end end def erase_temps() temp_list = [] path = @searchPathComboBox.text(0).strip Find.find(path) do |item| unless(FileTest.directory?(item)) if(item =~ %r{.*~$}) temp_list << item end end end @resultsTextEdit.setText(temp_list.sort.reverse.join("\n")) statusBar.message("Found #{temp_list.size} backup files.") reply = Qt::MessageBox::critical(self, self.className.to_s,"Warning: okay to erase " + temp_list.size.to_s + "\nbackup file(s)?","Yes","No","Cancel",1) if(reply == 0) temp_list.each do |path| File.delete(path) end @resultsTextEdit.setText("") end end def html_escape(s) s.gsub!("<","<") s.gsub!(">",">") return s end def update_combo_box(widget,array = nil) if(array) array.uniq! widget.clear() array.each do |item| widget.insertItem(item) end else # this section solves the problem that # the user may not have pressed "Enter" # before selecting an action line_edit = widget.lineEdit() if(line_edit) text = line_edit.text() else text = widget.currentText() end array = [] 0.upto(widget.count-1) do |i| array << widget.text(i) end # move selected text to top of list array.uniq! array.delete(text) array.unshift(text) widget.clear() array.each do |item| widget.insertItem(item) end end widget.setCurrentItem(0) s = widget.currentText() # will this string be difficult to read in a combobox? if(s && s.length > 16) tip = html_escape(s) Qt::ToolTip.add(widget,tip) elsif(widget.editable) Qt::ToolTip.add(widget,"Enter your search string here") else Qt::ToolTip.add(widget,"Select from this list or press \"Browse\".") end return array end def update_all_combo_boxes() update_combo_box(@fileFilterComboBox) update_combo_box(@searchPathComboBox) update_combo_box(@searchComboBox) update_combo_box(@replaceComboBox) end def quitButton_clicked(*k) close() end def searchReplaceButton_clicked(*k) search_replace() end def scanButton_clicked(*k) build_tree() end def choosePathButton_clicked(*k) choosePath() end def undoPushButton_clicked(*k) revert() end def searchPushButton_clicked(*k) find_matches() end def resultsTextEdit_clicked(*k) edit_file(k,@resultsTextEdit) end def changedTextEdit_clicked(*k) edit_file(k,@changedTextEdit) end def eraseButton_clicked(*k) erase_temps() end def searchPathComboBox_activated(*k) update_combo_box(@searchPathComboBox) end def fileFilterComboBox_activated(*k) update_combo_box(@fileFilterComboBox) end def searchComboBox_activated(*k) update_combo_box(@searchComboBox) end def replaceComboBox_activated(*k) update_combo_box(@replaceComboBox) end def helpSearchLineEdit_textChanged(*k) @help_engine.search end def helpSearchLineEdit_returnPressed(*k) @help_engine.search end def controlTabWidget_selected(*k) tab = k.first case tab when "Help" then @helpSearchLineEdit.setFocus() end end end # create and show application if $0 == __FILE__ app = Qt::Application.new(ARGV) dialog = SearchReplaceGlobal.new(app,ARGV) app.mainWidget = dialog dialog.show app.exec end