diff --git a/data/artwork/icons/App_TextSearch b/data/artwork/icons/App_TextSearch new file mode 100644 index 0000000000..1f598a08f3 Binary files /dev/null and b/data/artwork/icons/App_TextSearch differ diff --git a/src/add-ons/tracker/Jamfile b/src/add-ons/tracker/Jamfile index b798bc2301..fb88b0b11b 100644 --- a/src/add-ons/tracker/Jamfile +++ b/src/add-ons/tracker/Jamfile @@ -3,4 +3,5 @@ SubDir HAIKU_TOP src add-ons tracker ; SubInclude HAIKU_TOP src add-ons tracker zipomatic ; SubInclude HAIKU_TOP src add-ons tracker filetype ; SubInclude HAIKU_TOP src add-ons tracker mark_as ; +SubInclude HAIKU_TOP src add-ons tracker text_search ; diff --git a/src/add-ons/tracker/text_search/GlobalDefs.h b/src/add-ons/tracker/text_search/GlobalDefs.h new file mode 100644 index 0000000000..8a63685ef7 --- /dev/null +++ b/src/add-ons/tracker/text_search/GlobalDefs.h @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2008 Stephan Aßmus + * All rights reserved. Distributed under the terms of the MIT license. + */ +#ifndef GLOBAL_DEFS_H +#define GLOBAL_DEFS_H + +#define APP_SIGNATURE "application/x-vnd.mahlzeit.trackergrep" +#define APP_NAME "TextSearch" + +#endif // GLOBAL_DEFS_H diff --git a/src/add-ons/tracker/text_search/GrepApp.cpp b/src/add-ons/tracker/text_search/GrepApp.cpp new file mode 100644 index 0000000000..da55f68210 --- /dev/null +++ b/src/add-ons/tracker/text_search/GrepApp.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "GrepApp.h" + +#include + +#include + +#include "GlobalDefs.h" +#include "GrepWindow.h" +#include "Model.h" + + +GrepApp::GrepApp() + : BApplication(APP_SIGNATURE), + fGotArgvOnStartup(false), + fGotRefsOnStartup(false), + fQuitter(NULL) +{ +} + + +GrepApp::~GrepApp() +{ + delete fQuitter; +} + + +void +GrepApp::ArgvReceived(int32 argc, char** argv) +{ + fGotArgvOnStartup = true; + + BMessage message(B_REFS_RECEIVED); + int32 refCount = 0; + + for (int32 i = 1; i < argc; i++) { + BEntry entry(argv[i]); + entry_ref ref; + entry.GetRef(&ref); + + if (entry.Exists()) { + message.AddRef("refs", &ref); + refCount += 1; + } else + printf("%s: File not found: %s\n", argv[0], argv[i]); + } + + if (refCount > 0) + RefsReceived(&message); +} + + +void +GrepApp::RefsReceived(BMessage* message) +{ + if (IsLaunching()) + fGotRefsOnStartup = true; + + new GrepWindow(message); +} + + +void +GrepApp::ReadyToRun() +{ + if (!fGotArgvOnStartup && !fGotRefsOnStartup) + _NewUnfocusedGrepWindow(); + + // TODO: stippi: I don't understand what this is supposed to do: + if (fGotArgvOnStartup && !fGotRefsOnStartup) + PostMessage(B_QUIT_REQUESTED); +} + + +void +GrepApp::MessageReceived(BMessage* message) +{ + switch (message->what) { + case B_SILENT_RELAUNCH: + _NewUnfocusedGrepWindow(); + break; + + case MSG_TRY_QUIT: + _TryQuit(); + break; + + default: + BApplication::MessageReceived(message); + break; + } +} + + +void +GrepApp::_TryQuit() +{ + if (CountWindows() == 0) + PostMessage(B_QUIT_REQUESTED); + + if (CountWindows() == 1 && fQuitter == NULL) { + fQuitter = new BMessageRunner(be_app_messenger, + new BMessage(MSG_TRY_QUIT), 200000, -1); + } +} + + +void +GrepApp::_NewUnfocusedGrepWindow() +{ + BMessage emptyMessage; + new GrepWindow(&emptyMessage); +} diff --git a/src/add-ons/tracker/text_search/GrepApp.h b/src/add-ons/tracker/text_search/GrepApp.h new file mode 100644 index 0000000000..86483b8db7 --- /dev/null +++ b/src/add-ons/tracker/text_search/GrepApp.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include +#include + +#ifndef GREP_APP_H +#define GREP_APP_H + +class GrepApp : public BApplication { +public: + GrepApp(); + virtual ~GrepApp(); + + virtual void ArgvReceived(int32 argc, char** argv); + virtual void RefsReceived(BMessage* message); + virtual void MessageReceived(BMessage* message); + virtual void ReadyToRun(); + +private: + void _TryQuit(); + void _NewUnfocusedGrepWindow(); + + bool fGotArgvOnStartup; + bool fGotRefsOnStartup; + + BMessageRunner* fQuitter; +}; + +#endif // GREP_APP_H diff --git a/src/add-ons/tracker/text_search/GrepListView.cpp b/src/add-ons/tracker/text_search/GrepListView.cpp new file mode 100644 index 0000000000..4e855964d3 --- /dev/null +++ b/src/add-ons/tracker/text_search/GrepListView.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "GrepListView.h" + +#include + +ResultItem::ResultItem(const entry_ref& ref) + : BStringItem("", 0, false), + ref(ref) +{ + BEntry entry(&ref); + BPath path(&entry); + SetText(path.Path()); +} + + +GrepListView::GrepListView() + : BOutlineListView(BRect(0, 0, 40, 80), "SearchResults", + B_MULTIPLE_SELECTION_LIST, B_FOLLOW_ALL_SIDES, + B_WILL_DRAW | B_NAVIGABLE) +{ +} diff --git a/src/add-ons/tracker/text_search/GrepListView.h b/src/add-ons/tracker/text_search/GrepListView.h new file mode 100644 index 0000000000..61e0537303 --- /dev/null +++ b/src/add-ons/tracker/text_search/GrepListView.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef GREP_LIST_VIEW_H +#define GREP_LIST_VIEW_H + +#include +#include +#include + + +class ResultItem : public BStringItem { +public: + ResultItem(const entry_ref& ref); + + entry_ref ref; +}; + + +class GrepListView : public BOutlineListView { +public: + GrepListView(); +}; + +#endif // GREP_LIST_VIEW_H diff --git a/src/add-ons/tracker/text_search/GrepWindow.cpp b/src/add-ons/tracker/text_search/GrepWindow.cpp new file mode 100644 index 0000000000..cbc9f82a8e --- /dev/null +++ b/src/add-ons/tracker/text_search/GrepWindow.cpp @@ -0,0 +1,1596 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "GrepWindow.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GlobalDefs.h" +#include "Grepper.h" +#include "Translation.h" + +using std::nothrow; + + +GrepWindow::GrepWindow(BMessage* message) + : BWindow(BRect(0, 0, 1, 1), NULL, B_DOCUMENT_WINDOW, 0), + fSearchText(NULL), + fSearchResults(NULL), + fMenuBar(NULL), + fFileMenu(NULL), + fNew(NULL), + fOpen(NULL), + fClose(NULL), + fAbout(NULL), + fQuit(NULL), + fActionMenu(NULL), + fSelectAll(NULL), + fSearch(NULL), + fTrimSelection(NULL), + fCopyText(NULL), + fSelectInTracker(NULL), + fOpenSelection(NULL), + fPreferencesMenu(NULL), + fRecurseLinks(NULL), + fRecurseDirs(NULL), + fSkipDotDirs(NULL), + fCaseSensitive(NULL), + fEscapeText(NULL), + fTextOnly(NULL), + fInvokePe(NULL), + fShowLinesMenuitem(NULL), + fHistoryMenu(NULL), + fEncodingMenu(NULL), + fUTF8(NULL), + fShiftJIS(NULL), + fEUC(NULL), + fJIS(NULL), + + fShowLinesCheckbox(NULL), + fButton(NULL), + + fGrepper(NULL), + fOldPattern(""), + + fModel(new (nothrow) Model()), + + fFilePanel(NULL) +{ + if (fModel == NULL) + return; + + entry_ref directory; + _InitRefsReceived(&directory, message); + + fModel->fDirectory = directory; + fModel->fSelectedFiles = *message; + fModel->fTarget = this; + + _SetWindowTitle(); + _CreateMenus(); + _CreateViews(); + _LayoutViews(); + _LoadPrefs(); + _TileIfMultipleWindows(); + + Show(); +} + + +GrepWindow::~GrepWindow() +{ + if (fModel->fState == STATE_SEARCH) { + fGrepper->Cancel(); + } + + delete fModel; +} + + +void GrepWindow::FrameResized(float width, float height) +{ + BWindow::FrameResized(width, height); + fModel->fFrame = Frame(); + _SavePrefs(); +} + + +void GrepWindow::FrameMoved(BPoint origin) +{ + BWindow::FrameMoved(origin); + fModel->fFrame = Frame(); + _SavePrefs(); +} + + +void GrepWindow::MenusBeginning() +{ + fModel->FillHistoryMenu(fHistoryMenu); + BWindow::MenusBeginning(); +} + + +void GrepWindow::MenusEnded() +{ + for (int32 t = fHistoryMenu->CountItems(); t > 0; --t) + delete fHistoryMenu->RemoveItem(t - 1); + + BWindow::MenusEnded(); +} + + +void GrepWindow::MessageReceived(BMessage *message) +{ + switch (message->what) { + case B_ABOUT_REQUESTED: + _OnAboutRequested(); + break; + + case MSG_NEW_WINDOW: + _OnNewWindow(); + break; + + case B_SIMPLE_DATA: + _OnFileDrop(message); + break; + + case MSG_OPEN_PANEL: + _OnOpenPanel(); + break; + + case MSG_REFS_RECEIVED: + _OnRefsReceived(message); + break; + + case B_CANCEL: + _OnOpenPanelCancel(); + break; + + case MSG_RECURSE_LINKS: + _OnRecurseLinks(); + break; + + case MSG_RECURSE_DIRS: + _OnRecurseDirs(); + break; + + case MSG_SKIP_DOT_DIRS: + _OnSkipDotDirs(); + break; + + case MSG_CASE_SENSITIVE: + _OnCaseSensitive(); + break; + + case MSG_ESCAPE_TEXT: + _OnEscapeText(); + break; + + case MSG_TEXT_ONLY: + _OnTextOnly(); + break; + + case MSG_INVOKE_PE: + _OnInvokePe(); + break; + + case MSG_SEARCH_TEXT: + _OnSearchText(); + break; + + case MSG_SELECT_HISTORY: + _OnHistoryItem(message); + break; + + case MSG_START_CANCEL: + _OnStartCancel(); + break; + + case MSG_SEARCH_FINISHED: + _OnSearchFinished(); + break; + + case MSG_REPORT_FILE_NAME: + _OnReportFileName(message); + break; + + case MSG_REPORT_RESULT: + _OnReportResult(message); + break; + + case MSG_REPORT_ERROR: + _OnReportError(message); + break; + + case MSG_SELECT_ALL: + _OnSelectAll(message); + break; + + case MSG_TRIM_SELECTION: + _OnTrimSelection(); + break; + + case MSG_COPY_TEXT: + _OnCopyText(); + break; + + case MSG_SELECT_IN_TRACKER: + _OnSelectInTracker(); + break; + + case MSG_MENU_SHOW_LINES: + _OnMenuShowLines(); + break; + + case MSG_CHECKBOX_SHOW_LINES: + _OnCheckboxShowLines(); + break; + + case MSG_OPEN_SELECTION: + // fall through + case MSG_INVOKE_ITEM: + _OnInvokeItem(); + break; + + case MSG_QUIT_NOW: + _OnQuitNow(); + break; + + case 'utf8': + fModel->fEncoding = 0; + break; + + case B_SJIS_CONVERSION: + fModel->fEncoding = B_SJIS_CONVERSION; + break; + + case B_EUC_CONVERSION: + fModel->fEncoding = B_EUC_CONVERSION; + break; + + case B_JIS_CONVERSION: + fModel->fEncoding = B_JIS_CONVERSION; + break; + + default: + BWindow::MessageReceived(message); + break; + } +} + + +void +GrepWindow::Quit() +{ + _SavePrefs(); + + // TODO: stippi: Looks like this could be done + // by maintaining a counter in GrepApp with the number of open + // grep windows... and just quit when it goes zero + if (be_app->Lock()) { + be_app->PostMessage(MSG_TRY_QUIT); + be_app->Unlock(); + BWindow::Quit(); + } +} + + +// #pragma mark - + + +void +GrepWindow::_InitRefsReceived(entry_ref* directory, BMessage* message) +{ + // HACK-HACK-HACK: + // If the user selected a single folder and invoked TextSearch on it, + // but recurse directories is switched off, TextSearch would do nothing. + // In that special case, we'd like it to recurse into that folder (but + // not go any deeper after that). + + type_code code; + int32 count; + message->GetInfo("refs", &code, &count); + + if (count == 0) { + if (message->FindRef("dir_ref", 0, directory) == B_OK) + message->MakeEmpty(); + } + + if (count == 1) { + entry_ref ref; + if (message->FindRef("refs", 0, &ref) == B_OK) { + BEntry entry(&ref, true); + if (entry.IsDirectory()) { + // ok, special case, we use this folder as base directory + // and pretend nothing had been selected: + *directory = ref; + message->MakeEmpty(); + } + } + } +} + + +void +GrepWindow::_SetWindowTitle() +{ + BEntry entry(&fModel->fDirectory, true); + BString title; + if (entry.InitCheck() == B_OK) { + BPath path; + if (entry.GetPath(&path) == B_OK) + title << APP_NAME << ": " << path.Path(); + } + + if (!title.Length()) + title = APP_NAME; + + SetTitle(title.String()); +} + + +void +GrepWindow::_CreateMenus() +{ + fMenuBar = new BMenuBar(BRect(0,0,1,1), "menubar"); + + fFileMenu = new BMenu(_T("File")); + fActionMenu = new BMenu(_T("Actions")); + fPreferencesMenu = new BMenu(_T("Preferences")); + fHistoryMenu = new BMenu(_T("History")); + fEncodingMenu = new BMenu(_T("Encoding")); + + fNew = new BMenuItem( + _T("New Window"), new BMessage(MSG_NEW_WINDOW), 'N'); + + fOpen = new BMenuItem( + _T("Set Which Files to Search"), new BMessage(MSG_OPEN_PANEL), 'F'); + + fClose = new BMenuItem( + _T("Close"), new BMessage(B_QUIT_REQUESTED), 'W'); + + fAbout = new BMenuItem( + _T("About"), new BMessage(B_ABOUT_REQUESTED)); + + fQuit = new BMenuItem( + _T("Quit"), new BMessage(MSG_QUIT_NOW), 'Q'); + + fSearch = new BMenuItem( + _T("Search"), new BMessage(MSG_START_CANCEL), 'S'); + + fSelectAll = new BMenuItem( + _T("Select All"), new BMessage(MSG_SELECT_ALL), 'A'); + + fTrimSelection = new BMenuItem( + _T("Trim to Selection"), new BMessage(MSG_TRIM_SELECTION), 'T'); + + fOpenSelection = new BMenuItem( + _T("Open Selection"), new BMessage(MSG_OPEN_SELECTION), 'O'); + + fSelectInTracker = new BMenuItem( + _T("Show Files in Tracker"), new BMessage(MSG_SELECT_IN_TRACKER), 'K'); + + fCopyText = new BMenuItem( + _T("Copy Text to Clipboard"), new BMessage(MSG_COPY_TEXT), 'B'); + + fRecurseLinks = new BMenuItem( + _T("Follow symbolic links"), new BMessage(MSG_RECURSE_LINKS)); + + fRecurseDirs = new BMenuItem( + _T("Look in sub-directories"), new BMessage(MSG_RECURSE_DIRS)); + + fSkipDotDirs = new BMenuItem( + _T("Skip sub-directories starting with a dot"), new BMessage(MSG_SKIP_DOT_DIRS)); + + fCaseSensitive = new BMenuItem( + _T("Case sensitive"), new BMessage(MSG_CASE_SENSITIVE)); + + fEscapeText = new BMenuItem( + _T("Escape search text"), new BMessage(MSG_ESCAPE_TEXT)); + + fTextOnly = new BMenuItem( + _T("Text files only"), new BMessage(MSG_TEXT_ONLY)); + + fInvokePe = new BMenuItem( + _T("Open files in Pe"), new BMessage(MSG_INVOKE_PE)); + + fShowLinesMenuitem = new BMenuItem( + _T("Show Lines"), new BMessage(MSG_MENU_SHOW_LINES), 'L'); + fShowLinesMenuitem->SetMarked(true); + + fUTF8 = new BMenuItem("UTF8", new BMessage('utf8')); + fShiftJIS = new BMenuItem("ShiftJIS", new BMessage(B_SJIS_CONVERSION)); + fEUC = new BMenuItem("EUC", new BMessage(B_EUC_CONVERSION)); + fJIS = new BMenuItem("JIS", new BMessage(B_JIS_CONVERSION)); + + fFileMenu->AddItem(fNew); + fFileMenu->AddSeparatorItem(); + fFileMenu->AddItem(fOpen); + fFileMenu->AddItem(fClose); + fFileMenu->AddSeparatorItem(); + fFileMenu->AddItem(fAbout); + fFileMenu->AddSeparatorItem(); + fFileMenu->AddItem(fQuit); + + fActionMenu->AddItem(fSearch); + fActionMenu->AddSeparatorItem(); + fActionMenu->AddItem(fSelectAll); + fActionMenu->AddItem(fTrimSelection); + fActionMenu->AddSeparatorItem(); + fActionMenu->AddItem(fOpenSelection); + fActionMenu->AddItem(fSelectInTracker); + fActionMenu->AddItem(fCopyText); + + fPreferencesMenu->AddItem(fRecurseLinks); + fPreferencesMenu->AddItem(fRecurseDirs); + fPreferencesMenu->AddItem(fSkipDotDirs); + fPreferencesMenu->AddItem(fCaseSensitive); + fPreferencesMenu->AddItem(fEscapeText); + fPreferencesMenu->AddItem(fTextOnly); + fPreferencesMenu->AddItem(fInvokePe); + fPreferencesMenu->AddSeparatorItem(); + fPreferencesMenu->AddItem(fShowLinesMenuitem); + + fEncodingMenu->AddItem(fUTF8); + fEncodingMenu->AddItem(fShiftJIS); + fEncodingMenu->AddItem(fEUC); + fEncodingMenu->AddItem(fJIS); + +// fEncodingMenu->SetLabelFromMarked(true); + // Do we really want this ? + fEncodingMenu->SetRadioMode(true); + fEncodingMenu->ItemAt(0)->SetMarked(true); + + fMenuBar->AddItem(fFileMenu); + fMenuBar->AddItem(fActionMenu); + fMenuBar->AddItem(fPreferencesMenu); + fMenuBar->AddItem(fHistoryMenu); + fMenuBar->AddItem(fEncodingMenu); + + AddChild(fMenuBar); + SetKeyMenuBar(fMenuBar); + + fSearch->SetEnabled(false); +} + + +void +GrepWindow::_CreateViews() +{ + // The search pattern entry field does not send a message when + // is pressed, because the "Search/Cancel" button already + // does this and we don't want to send the same message twice. + + fSearchText = new BTextControl( + BRect(0, 0, 0, 1), "SearchText", NULL, NULL, NULL, + B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP, + B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_NAVIGABLE); + + fSearchText->TextView()->SetMaxBytes(1000); + fSearchText->ResizeToPreferred(); + // because it doesn't have a label + + fSearchText->SetModificationMessage(new BMessage(MSG_SEARCH_TEXT)); + + fButton = new BButton( + BRect(0, 1, 80, 1), "Button", _T("Search"), + new BMessage(MSG_START_CANCEL), B_FOLLOW_RIGHT); + + fButton->MakeDefault(true); + fButton->ResizeToPreferred(); + fButton->SetEnabled(false); + + fShowLinesCheckbox = new BCheckBox( + BRect(0, 0, 1, 1), "ShowLines", _T("Show Lines"), + new BMessage(MSG_CHECKBOX_SHOW_LINES), B_FOLLOW_LEFT); + + fShowLinesCheckbox->SetValue(B_CONTROL_ON); + fShowLinesCheckbox->ResizeToPreferred(); + + fSearchResults = new GrepListView(); + + fSearchResults->SetInvocationMessage(new BMessage(MSG_INVOKE_ITEM)); + fSearchResults->ResizeToPreferred(); +} + + +void +GrepWindow::_LayoutViews() +{ + float menubarWidth, menubarHeight = 20; + fMenuBar->GetPreferredSize(&menubarWidth, &menubarHeight); + + BBox *background = new BBox( + BRect(0, menubarHeight + 1, 2, menubarHeight + 2), B_EMPTY_STRING, + B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP, + B_WILL_DRAW | B_FRAME_EVENTS | B_NAVIGABLE_JUMP, + B_PLAIN_BORDER); + + BScrollView *scroller = new BScrollView( + "ScrollSearchResults", fSearchResults, B_FOLLOW_ALL_SIDES, + B_FULL_UPDATE_ON_RESIZE, true, true, B_NO_BORDER); + + scroller->ResizeToPreferred(); + + float width = 8 + fShowLinesCheckbox->Frame().Width() + + 8 + fButton->Frame().Width() + 8; + + float height = 8 + fSearchText->Frame().Height() + 8 + + fButton->Frame().Height() + 8 + scroller->Frame().Height(); + + float backgroundHeight = 8 + fSearchText->Frame().Height() + + 8 + fButton->Frame().Height() + 8; + + ResizeTo(width, height); + + AddChild(background); + background->ResizeTo(width, backgroundHeight); + background->AddChild(fSearchText); + background->AddChild(fShowLinesCheckbox); + background->AddChild(fButton); + + fSearchText->MoveTo(8, 8); + fSearchText->ResizeBy(width - 16, 0); + fSearchText->MakeFocus(true); + + fShowLinesCheckbox->MoveTo( + 8, 8 + fSearchText->Frame().Height() + 8 + + (fButton->Frame().Height() - fShowLinesCheckbox->Frame().Height())/2); + + fButton->MoveTo( + width - fButton->Frame().Width() - 8, + 8 + fSearchText->Frame().Height() + 8); + + AddChild(scroller); + scroller->MoveTo(0, menubarHeight + 1 + backgroundHeight + 1); + scroller->ResizeTo(width + 1, height - backgroundHeight - menubarHeight - 1); + + BRect screenRect = BScreen(this).Frame(); + + MoveTo( + (screenRect.Width() - width) / 2, + (screenRect.Height() - height) / 2); + + SetSizeLimits(width, 10000, height, 10000); +} + + +void +GrepWindow::_TileIfMultipleWindows() +{ + if (be_app->Lock()) { + int32 windowCount = be_app->CountWindows(); + be_app->Unlock(); + + if (windowCount > 1) + MoveBy(20,20); + } + + BScreen screen(this); + BRect screenFrame = screen.Frame(); + BRect windowFrame = Frame(); + + if (windowFrame.left > screenFrame.right + || windowFrame.top > screenFrame.bottom + || windowFrame.right < screenFrame.left + || windowFrame.bottom < screenFrame.top) + MoveTo(50,50); +} + + +// #pragma mark - + + +void +GrepWindow::_LoadPrefs() +{ + Lock(); + + fModel->LoadPrefs(); + + fRecurseDirs->SetMarked(fModel->fRecurseDirs); + fRecurseLinks->SetMarked(fModel->fRecurseLinks); + fSkipDotDirs->SetMarked(fModel->fSkipDotDirs); + fCaseSensitive->SetMarked(fModel->fCaseSensitive); + fEscapeText->SetMarked(fModel->fEscapeText); + fTextOnly->SetMarked(fModel->fTextOnly); + fInvokePe->SetMarked(fModel->fInvokePe); + + fShowLinesCheckbox->SetValue( + fModel->fShowContents ? B_CONTROL_ON : B_CONTROL_OFF); + fShowLinesMenuitem->SetMarked( + fModel->fShowContents ? true : false); + + switch (fModel->fEncoding) { + case 0: + fUTF8->SetMarked(true); + break; + case B_SJIS_CONVERSION: + fShiftJIS->SetMarked(true); + break; + case B_EUC_CONVERSION: + fEUC->SetMarked(true); + break; + case B_JIS_CONVERSION: + fJIS->SetMarked(true); + break; + default: + printf("Woops. Bad fModel->fEncoding value.\n"); + break; + } + + MoveTo(fModel->fFrame.left, fModel->fFrame.top); + ResizeTo(fModel->fFrame.Width(), fModel->fFrame.Height()); + + Unlock(); +} + + +void +GrepWindow::_SavePrefs() +{ + fModel->SavePrefs(); +} + + +// #pragma mark - events + + +void +GrepWindow::_OnStartCancel() +{ + if (fModel->fState == STATE_IDLE) { + fModel->fState = STATE_SEARCH; + + fSearchResults->MakeEmpty(); + + if (fSearchText->TextView()->TextLength() == 0) + return; + + fModel->AddToHistory(fSearchText->Text()); + + // From now on, we don't want to be notified when the + // search pattern changes, because the control will be + // displaying the names of the files we are grepping. + + fSearchText->SetModificationMessage(NULL); + + fFileMenu->SetEnabled(false); + fActionMenu->SetEnabled(false); + fPreferencesMenu->SetEnabled(false); + fHistoryMenu->SetEnabled(false); + fEncodingMenu->SetEnabled(false); + + fSearchText->SetEnabled(false); + + fButton->MakeFocus(true); + fButton->SetLabel(_T("Cancel")); + fSearch->SetEnabled(false); + + // We need to remember the search pattern, because during + // the grepping, the text control's text will be replaced + // by the name of the file that's currently being grepped. + // When the grepping finishes, we need to restore the old + // search pattern. + + fOldPattern = fSearchText->Text(); + + fGrepper = new Grepper(fOldPattern.String(), fModel); + fGrepper->Start(); + } else if (fModel->fState == STATE_SEARCH) { + fModel->fState = STATE_CANCEL; + fGrepper->Cancel(); + } +} + + +void +GrepWindow::_OnSearchFinished() +{ + fModel->fState = STATE_IDLE; + + delete fGrepper; + fGrepper = NULL; + + fFileMenu->SetEnabled(true); + fActionMenu->SetEnabled(true); + fPreferencesMenu->SetEnabled(true); + fHistoryMenu->SetEnabled(true); + fEncodingMenu->SetEnabled(true); + + fButton->SetLabel(_T("Search")); + fButton->SetEnabled(true); + fSearch->SetEnabled(true); + + fSearchText->SetEnabled(true); + fSearchText->MakeFocus(true); + fSearchText->SetText(fOldPattern.String()); + fSearchText->TextView()->SelectAll(); + fSearchText->SetModificationMessage(new BMessage(MSG_SEARCH_TEXT)); +} + + +void +GrepWindow::_OnReportFileName(BMessage* message) +{ + fSearchText->SetText(message->FindString("filename")); +} + + +void +GrepWindow::_OnReportResult(BMessage* message) +{ + entry_ref ref; + if (message->FindRef("ref", &ref) != B_OK) + return; + + BStringItem* item = new ResultItem(ref); + fSearchResults->AddItem(item); + item->SetExpanded(fModel->fShowContents); + + type_code type; + int32 count; + message->GetInfo("text", &type, &count); + + const char* buf; + while (message->FindString("text", --count, &buf) == B_OK) { + uchar* temp = (uchar*)strdup(buf); + uchar* ptr = temp; + + while (true) { + // replace all non-printable characters by spaces + uchar c = *ptr; + + if (c == '\0') + break; + + if (!(c & 0x80) && iscntrl(c)) + *ptr = ' '; + + ++ptr; + } + + fSearchResults->AddUnder( + new BStringItem((const char*)temp), item); + + free(temp); + } +} + + +void +GrepWindow::_OnReportError(BMessage *message) +{ + const char* buf; + if (message->FindString("error", &buf) == B_OK) + fSearchResults->AddItem(new BStringItem(buf)); +} + + +void +GrepWindow::_OnRecurseLinks() +{ + fModel->fRecurseLinks = !fModel->fRecurseLinks; + fRecurseLinks->SetMarked(fModel->fRecurseLinks); + _SavePrefs(); +} + + +void +GrepWindow::_OnRecurseDirs() +{ + fModel->fRecurseDirs = !fModel->fRecurseDirs; + fRecurseDirs->SetMarked(fModel->fRecurseDirs); + _SavePrefs(); +} + + +void +GrepWindow::_OnSkipDotDirs() +{ + fModel->fSkipDotDirs = !fModel->fSkipDotDirs; + fSkipDotDirs->SetMarked(fModel->fSkipDotDirs); + _SavePrefs(); +} + + +void +GrepWindow::_OnEscapeText() +{ + fModel->fEscapeText = !fModel->fEscapeText; + fEscapeText->SetMarked(fModel->fEscapeText); + _SavePrefs(); +} + + +void +GrepWindow::_OnCaseSensitive() +{ + fModel->fCaseSensitive = !fModel->fCaseSensitive; + fCaseSensitive->SetMarked(fModel->fCaseSensitive); + _SavePrefs(); +} + + +void +GrepWindow::_OnTextOnly() +{ + fModel->fTextOnly = !fModel->fTextOnly; + fTextOnly->SetMarked(fModel->fTextOnly); + _SavePrefs(); +} + + +void +GrepWindow::_OnInvokePe() +{ + fModel->fInvokePe = !fModel->fInvokePe; + fInvokePe->SetMarked(fModel->fInvokePe); + _SavePrefs(); +} + + +void +GrepWindow::_OnCheckboxShowLines() +{ + // toggle checkbox and menuitem + fModel->fShowContents = !fModel->fShowContents; + fShowLinesMenuitem->SetMarked(!fShowLinesMenuitem->IsMarked()); + + // Selection in BOutlineListView in multiple selection mode + // gets weird when collapsing. I've tried all sorts of things. + // It seems impossible to make it behave just right. + + // Going from collapsed to expande mode, the superitems + // keep their selection, the subitems don't (yet) have + // a selection. This works as expected, AFAIK. + + // Going from expanded to collapsed mode, I would like + // for a selected subitem (line) to select its superitem, + // (its file) and the subitem be unselected. + + // I've successfully tried code patches that apply the + // selection pattern that I want, but with weird effects + // on subsequent manual selection. + // Lines stay selected while the user tries to select + // some other line. It just gets weird. + + // It's as though listItem->Select() and Deselect() + // put the items in some semi-selected state. + // Or maybe I've got it all wrong. + + // So, here's the plain basic collapse/expand. + // I think it's the least bad of what's possible on BeOS R5, + // but perhaps someone comes along with a patch of magic. + + int32 numItems = fSearchResults->FullListCountItems(); + for (int32 x = 0; x < numItems; ++x) { + BListItem* listItem = fSearchResults->FullListItemAt(x); + if (listItem->OutlineLevel() == 0) { + if (fModel->fShowContents) { + if (!fSearchResults->IsExpanded(x)) + fSearchResults->Expand(listItem); + } else { + if (fSearchResults->IsExpanded(x)) + fSearchResults->Collapse(listItem); + } + } + } + + fSearchResults->Invalidate(); + + _SavePrefs(); +} + + +void +GrepWindow::_OnMenuShowLines() +{ + // toggle companion checkbox + fShowLinesCheckbox->SetValue(!fShowLinesCheckbox->Value()); + _OnCheckboxShowLines(); +} + + +void +GrepWindow::_OnInvokeItem() +{ + for (int32 selectionIndex = 0; ; selectionIndex++) { + int32 itemIndex = fSearchResults->CurrentSelection(selectionIndex); + BListItem* item = fSearchResults->ItemAt(itemIndex); + if (item == NULL) + break; + + int32 level = item->OutlineLevel(); + int32 lineNum = -1; + + // Get the line number. + // only this level has line numbers + if (level == 1) { + BStringItem *str = dynamic_cast(item); + if (str != NULL) { + lineNum = atol(str->Text()); + // fortunately, atol knows when to stop the conversion + } + } + + // Get the top-most item and launch its entry_ref. + while (level != 0) { + item = fSearchResults->Superitem(item); + if (item == NULL) + break; + level = item->OutlineLevel(); + } + + ResultItem* entry = dynamic_cast(item); + if (entry != NULL) { + bool done = false; + + if (fModel->fInvokePe) + done = _OpenInPe(entry->ref, lineNum); + + if (!done) + be_roster->Launch(&entry->ref); + } + } +} + + +void +GrepWindow::_OnSearchText() +{ + fButton->SetEnabled(fSearchText->TextView()->TextLength() != 0); + fSearch->SetEnabled(fSearchText->TextView()->TextLength() != 0); +} + + +void +GrepWindow::_OnHistoryItem(BMessage* message) +{ + const char* buf; + if (message->FindString("text", &buf) == B_OK) + fSearchText->SetText(buf); +} + + +void +GrepWindow::_OnTrimSelection() +{ + if (fSearchResults->CurrentSelection() < 0) { + BString text; + text << _T("Please select the files you wish to keep searching."); + text << "\n"; + text << _T("The unselected files will be removed from the list."); + text << "\n"; + BAlert* alert = new BAlert(NULL, text.String(), _T("Okay"), NULL, NULL, + B_WIDTH_AS_USUAL, B_WARNING_ALERT); + alert->Go(NULL); + return; + } + + BMessage message; + BString path; + + for (int32 index = 0; ; index++) { + BStringItem* item = dynamic_cast( + fSearchResults->ItemAt(index)); + if (item == NULL) + break; + + if (!item->IsSelected() || item->OutlineLevel() != 0) + continue; + + if (path == item->Text()) + continue; + + path = item->Text(); + entry_ref ref; + if (get_ref_for_path(path.String(), &ref) == B_OK) + message.AddRef("refs", &ref); + } + + fModel->fDirectory = entry_ref(); + // invalidated on purpose + + fModel->fSelectedFiles.MakeEmpty(); + fModel->fSelectedFiles = message; + + PostMessage(MSG_START_CANCEL); + + _SetWindowTitle(); +} + + +void +GrepWindow::_OnCopyText() +{ + bool onlyCopySelection = true; + + if (fSearchResults->CurrentSelection() < 0) + onlyCopySelection = false; + + BString buffer; + + for (int32 index = 0; ; index++) { + BStringItem* item = dynamic_cast( + fSearchResults->ItemAt(index)); + if (item == NULL) + break; + + if (onlyCopySelection) { + if (item->IsSelected()) + buffer << item->Text() << "\n"; + } else + buffer << item->Text() << "\n"; + } + + status_t status = B_OK; + + BMessage* clip = NULL; + + if (be_clipboard->Lock()) { + be_clipboard->Clear(); + + clip = be_clipboard->Data(); + + clip->AddData("text/plain", B_MIME_TYPE, buffer.String(), + buffer.Length()); + + status = be_clipboard->Commit(); + + if (status != B_OK) { + be_clipboard->Unlock(); + return; + } + + be_clipboard->Unlock(); + } +} + + +void +GrepWindow::_OnSelectInTracker() +{ + if (fSearchResults->CurrentSelection() < 0) { + BAlert* alert = new BAlert("Info", + _T("Please select the files you wish to have selected for you in " + "Tracker."), + _T("Okay"), NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT); + alert->Go(NULL); + return; + } + + BMessage message; + BString filePath; + BPath folderPath; + BList folderList; + BString lastFolderAddedToList; + + for (int32 index = 0; ; index++) { + BStringItem* item = dynamic_cast( + fSearchResults->ItemAt(index)); + if (item == NULL) + break; + + // only open selected and top level (file) items + if (!item->IsSelected() || item->OutlineLevel() > 0) + continue; + + // check if this was previously opened + if (filePath == item->Text()) + continue; + + filePath = item->Text(); + entry_ref file_ref; + if (get_ref_for_path(filePath.String(), &file_ref) != B_OK) + continue; + + message.AddRef("refs", &file_ref); + + // add parent folder to list of folders to open + folderPath.SetTo(filePath.String()); + if (folderPath.GetParent(&folderPath) == B_OK) { + BPath* path = new BPath(folderPath); + if (path->Path() != lastFolderAddedToList) { + // catches some duplicates + folderList.AddItem(path); + lastFolderAddedToList = path->Path(); + } else + delete path; + } + } + + _RemoveFolderListDuplicates(&folderList); + _OpenFoldersInTracker(&folderList); + + int32 aShortWhile = 100000; + snooze(aShortWhile); + + if (!_AreAllFoldersOpenInTracker(&folderList)) { + for (int32 x = 0; x < 5; x++) { + aShortWhile += 100000; + snooze(aShortWhile); + _OpenFoldersInTracker(&folderList); + } + } + + if (!_AreAllFoldersOpenInTracker(&folderList)) { + BAlert* alert = new BAlert(NULL, + _T("Tracker Grep couldn't open one or more folders, and it's very " + "sorry about it."), + _T("Forgive and forget!"), NULL, NULL, B_WIDTH_AS_USUAL, + B_STOP_ALERT); + alert->Go(NULL); + goto out; + } + + _SelectFilesInTracker(&folderList, &message); + +out: + // delete folderList contents + int32 folderCount = folderList.CountItems(); + for (int32 x = 0; x < folderCount; x++) + delete static_cast(folderList.ItemAt(x)); +} + + +void +GrepWindow::_OnQuitNow() +{ + if (be_app->Lock()) { + be_app->PostMessage(B_QUIT_REQUESTED); + be_app->Unlock(); + } +} + + +void +GrepWindow::_OnAboutRequested() +{ + app_info appInfo; + version_info vInfo; + BFile appFile; + BAppFileInfo appFileInfo; + BString fAppVersion; + + if (be_app->Lock()) { + be_app->GetAppInfo(&appInfo); + appFile.SetTo(&appInfo.ref, B_READ_ONLY); + appFileInfo.SetTo(&appFile); + if (appFileInfo.GetVersionInfo(&vInfo, B_APP_VERSION_KIND) == B_OK) + fAppVersion = BString("") << vInfo.major << '.' << vInfo.middle; + be_app->Unlock(); + } + + BString text; + text << APP_NAME << " " << fAppVersion << "\n"; + int32 titleLength = text.Length(); + text << _T("Get a grip on grep.") << "\n\n"; + text << _T(APP_NAME " lets you search the contents of your files.") << " "; + text << _T("It is primarily intended for use with text files.") << "\n\n"; + text << _T("Created by Matthijs Hollemans") << "\n"; + text << "mahlzeit@users.sf.net" << "\n\n"; + text << _T("Maintained by Jonas Sundström") << "\n"; + text << "jonas@kirilla.com" << "\n\n"; + text << _T("Contributed to by: "); + text << _T("Peter Hinely, Serge Fantino, Hideki Naito, Oscar Lesta, " + "Oliver Tappe, Luc Schrijvers and momoziro."); + text << "\n"; + + BAlert* alert = new BAlert("Tracker Grep", text.String(), _T("Ok"), NULL, + NULL, B_WIDTH_AS_USUAL, B_INFO_ALERT); + + BTextView* view = alert->TextView(); + BFont font; + view->SetStylable(true); + view->GetFont(&font); + font.SetSize(font.Size() * 1.5); + font.SetFace(B_BOLD_FACE); + view->SetFontAndColor(0, titleLength, &font); + + alert->Go(NULL); +} + + +void +GrepWindow::_OnFileDrop(BMessage* message) +{ + if (fModel->fState != STATE_IDLE) + return; + + entry_ref directory; + _InitRefsReceived(&directory, message); + + fModel->fDirectory = directory; + fModel->fSelectedFiles.MakeEmpty(); + fModel->fSelectedFiles = *message; + + fSearchResults->MakeEmpty(); + + _SetWindowTitle(); +} + + +void +GrepWindow::_OnRefsReceived(BMessage* message) +{ + _OnFileDrop(message); + // It seems a B_CANCEL always follows a B_REFS_RECEIVED + // from a BFilePanel in Open mode. + // + // _OnOpenPanelCancel() is called on B_CANCEL. + // That's where saving the current dir of the file panel occurs, for now, + // and also the neccesary deletion of the file panel object. + // A hidden file panel would otherwise jam the shutdown process. +} + + +void +GrepWindow::_OnOpenPanel() +{ + if (fFilePanel != NULL) + return; + + entry_ref path; + if (get_ref_for_path(fModel->fFilePanelPath.String(), &path) != B_OK) + return; + + fFilePanel = new BFilePanel(B_OPEN_PANEL, new BMessenger(NULL, this), + &path, B_FILE_NODE|B_DIRECTORY_NODE|B_SYMLINK_NODE, + true, new BMessage(MSG_REFS_RECEIVED), NULL, true, true); + + fFilePanel->Show(); +} + + +void +GrepWindow::_OnOpenPanelCancel() +{ + entry_ref panelDirRef; + fFilePanel->GetPanelDirectory(&panelDirRef); + BPath path(&panelDirRef); + fModel->fFilePanelPath = path.Path(); + delete fFilePanel; + fFilePanel = NULL; +} + + +void +GrepWindow::_OnSelectAll(BMessage *message) +{ + BMessenger messenger(fSearchResults); + messenger.SendMessage(B_SELECT_ALL); +} + + +void +GrepWindow::_OnNewWindow() +{ + BMessage cloneRefs; + // we don't want GrepWindow::InitRefsReceived() + // to mess with the refs of the current window + + cloneRefs = fModel->fSelectedFiles; + cloneRefs.AddRef("dir_ref", &(fModel->fDirectory)); + + new GrepWindow(&cloneRefs); +} + + +// #pragma mark - + + +bool +GrepWindow::_OpenInPe(const entry_ref &ref, int32 lineNum) +{ + BMessage message('Cmdl'); + message.AddRef("refs", &ref); + + if (lineNum != -1) + message.AddInt32("line", lineNum); + + entry_ref pe; + if (be_roster->FindApp(PE_SIGNATURE, &pe) != B_OK) + return false; + + if (be_roster->IsRunning(&pe)) { + BMessenger msngr(NULL, be_roster->TeamFor(&pe)); + if (msngr.SendMessage(&message) != B_OK) + return false; + } else { + if (be_roster->Launch(&pe, &message) != B_OK) + return false; + } + + return true; +} + + +void +GrepWindow::_RemoveFolderListDuplicates(BList* folderList) +{ + if (folderList == NULL) + return; + + int32 folderCount = folderList->CountItems(); + BString folderX; + BString folderY; + + for (int32 x = 0; x < folderCount; x++) { + BPath* path = static_cast(folderList->ItemAt(x)); + folderX = path->Path(); + + for (int32 y = x + 1; y < folderCount; y++) { + path = static_cast(folderList->ItemAt(y)); + folderY = path->Path(); + if (folderX == folderY) { + delete static_cast(folderList->RemoveItem(y)); + folderCount--; + y--; + } + } + } +} + + +status_t +GrepWindow::_OpenFoldersInTracker(BList* folderList) +{ + status_t status = B_OK; + BMessage refsMsg(B_REFS_RECEIVED); + + int32 folderCount = folderList->CountItems(); + for (int32 index = 0; index < folderCount; index++) { + BPath* path = static_cast(folderList->ItemAt(index)); + + entry_ref folderRef; + status = get_ref_for_path(path->Path(), &folderRef); + if (status != B_OK) + return status; + + status = refsMsg.AddRef("refs", &folderRef); + if (status != B_OK) + return status; + } + + status = be_roster->Launch(TRACKER_SIGNATURE, &refsMsg); + if (status != B_OK && status != B_ALREADY_RUNNING) + return status; + + return B_OK; +} + + +bool +GrepWindow::_AreAllFoldersOpenInTracker(BList *folderList) +{ + // Compare the folders we want open in Tracker to + // the actual Tracker windows currently open. + + // We build a list of open Tracker windows, and compare + // it to the list of folders we want open in Tracker. + + // If all folders exists in the list of Tracker windows + // return true + + status_t status = B_OK; + BMessenger trackerMessenger(TRACKER_SIGNATURE); + BMessage sendMessage; + BMessage replyMessage; + BList windowList; + + if (!trackerMessenger.IsValid()) + return false; + + for (int32 count = 1; ; count++) { + sendMessage.MakeEmpty(); + replyMessage.MakeEmpty(); + + sendMessage.what = B_GET_PROPERTY; + sendMessage.AddSpecifier("Path"); + sendMessage.AddSpecifier("Poses"); + sendMessage.AddSpecifier("Window", count); + + status = trackerMessenger.SendMessage(&sendMessage, &replyMessage); + + if (status != B_OK) + return false; + + entry_ref *tracker_ref = new entry_ref; + status = replyMessage.FindRef("result", tracker_ref); + + if (status == B_OK) + windowList.AddItem(static_cast(tracker_ref)); + + if (status != B_OK) + break; + } + + int32 folderCount = folderList->CountItems(); + int32 windowCount = windowList.CountItems(); + + int32 found = 0; + BPath* folderPath; + entry_ref* windowRef; + BString folderString; + BString windowString; + + if (folderCount > windowCount) + // at least one folder is not open in Tracker + return false; + + // Loop over the two lists and see if all folders exist as window + for (int32 x = 0; x < folderCount; x++) { + for (int32 y = 0; y < windowCount; y++) { + + folderPath = static_cast(folderList->ItemAt(x)); + windowRef = static_cast(windowList.ItemAt(y)); + + if (folderPath == NULL) + break; + + if (windowRef == NULL) + break; + + folderString = folderPath->Path(); + + BEntry entry; + BPath path; + + if (entry.SetTo(windowRef) == B_OK && path.SetTo(&entry) == B_OK) { + + windowString = path.Path(); + + if (folderString == windowString) { + found++; + break; + } + } + } + } + + // delete list of window entry_refs + for (int32 x = 0; x < windowCount; x++) + delete static_cast(windowList.ItemAt(x)); + + windowList.MakeEmpty(); + + if (found == folderCount) + return true; + + return false; +} + + +status_t +GrepWindow::_SelectFilesInTracker(BList* folderList, BMessage* refsMessage) +{ + // loops over Tracker windows, find each windowRef, + // extract the refs that are children of windowRef, + // add refs to selection-message + + status_t status = B_OK; + BMessenger trackerMessenger(TRACKER_SIGNATURE); + BMessage windowSendMessage; + BMessage windowReplyMessage; + BMessage selectionSendMessage; + BMessage selectionReplyMessage; + + if (!trackerMessenger.IsValid()) + return status; + + // loop over Tracker windows + for (int32 windowCount = 1; ; windowCount++) { + + windowSendMessage.MakeEmpty(); + windowReplyMessage.MakeEmpty(); + + windowSendMessage.what = B_GET_PROPERTY; + windowSendMessage.AddSpecifier("Path"); + windowSendMessage.AddSpecifier("Poses"); + windowSendMessage.AddSpecifier("Window", windowCount); + + status = trackerMessenger.SendMessage(&windowSendMessage, + &windowReplyMessage); + + if (status != B_OK) + return status; + + entry_ref *windowRef = new entry_ref; + status = windowReplyMessage.FindRef("result", windowRef); + + if (status != B_OK) + break; + + int32 folderCount = folderList->CountItems(); + + // loop over folders in folderList + for (int32 x = 0; x < folderCount; x++) { + BPath* folderPath = static_cast(folderList->ItemAt(x)); + if (folderPath == NULL) + break; + + BString folderString = folderPath->Path(); + + BEntry windowEntry; + BPath windowPath; + BString windowString; + + status = windowEntry.SetTo(windowRef); + if (status != B_OK) + break; + + status = windowPath.SetTo(&windowEntry); + if (status != B_OK) + break; + + windowString = windowPath.Path(); + + // if match, loop over items in refsMessage + // and add those that live in window/folder + // to a selection message + + if (windowString == folderString) { + selectionSendMessage.MakeEmpty(); + selectionSendMessage.what = B_SET_PROPERTY; + selectionReplyMessage.MakeEmpty(); + + // loop over refs and add to message + entry_ref ref; + for (int32 index = 0; ; index++) { + status = refsMessage->FindRef("refs", index, &ref); + if (status != B_OK) + break; + + BDirectory directory(windowRef); + BEntry entry(&ref); + if (directory.Contains(&entry)) + selectionSendMessage.AddRef("data", &ref); + } + + // finish selection message + selectionSendMessage.AddSpecifier("Selection"); + selectionSendMessage.AddSpecifier("Poses"); + selectionSendMessage.AddSpecifier("Window", windowCount); + + trackerMessenger.SendMessage(&selectionSendMessage, + &selectionReplyMessage); + } + } + } + + return B_OK; +} diff --git a/src/add-ons/tracker/text_search/GrepWindow.h b/src/add-ons/tracker/text_search/GrepWindow.h new file mode 100644 index 0000000000..9381b508e7 --- /dev/null +++ b/src/add-ons/tracker/text_search/GrepWindow.h @@ -0,0 +1,138 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef GREP_WINDOW_H +#define GREP_WINDOW_H + +#include +#include + +#include "Model.h" +#include "GrepListView.h" + +class Grepper; + +class GrepWindow : public BWindow { +public: + GrepWindow(BMessage* message); + virtual ~GrepWindow(); + + virtual void FrameResized(float width, float height); + virtual void FrameMoved(BPoint origin); + virtual void MenusBeginning(); + virtual void MenusEnded(); + virtual void MessageReceived(BMessage* message); + virtual void Quit(); + +private: + void _InitRefsReceived(entry_ref* directory, + BMessage* message); + void _SetWindowTitle(); + void _CreateMenus(); + void _CreateViews(); + void _LayoutViews(); + void _TileIfMultipleWindows(); + + void _LoadPrefs(); + void _SavePrefs(); + + void _OnStartCancel(); + void _OnSearchFinished(); + void _OnReportFileName(BMessage* message); + void _OnReportResult(BMessage* message); + void _OnReportError(BMessage* message); + void _OnRecurseLinks(); + void _OnRecurseDirs(); + void _OnSkipDotDirs(); + void _OnEscapeText(); + void _OnCaseSensitive(); + void _OnTextOnly(); + void _OnInvokePe(); + void _OnCheckboxShowLines(); + void _OnMenuShowLines(); + void _OnInvokeItem(); + void _OnSearchText(); + void _OnHistoryItem(BMessage* message); + void _OnTrimSelection(); + void _OnCopyText(); + void _OnSelectInTracker(); + void _OnQuitNow(); + void _OnAboutRequested(); + void _OnFileDrop(BMessage* message); + void _OnRefsReceived(BMessage* message); + void _OnOpenPanel(); + void _OnOpenPanelCancel(); + void _OnSelectAll(BMessage* message); + void _OnNewWindow(); + + bool _OpenInPe(const entry_ref& ref, int32 lineNum); + void _RemoveFolderListDuplicates(BList* folderList); + status_t _OpenFoldersInTracker(BList* folderList); + bool _AreAllFoldersOpenInTracker(BList* folderList); + status_t _SelectFilesInTracker(BList* folderList, + BMessage* refsMessage); + +private: + BTextControl* fSearchText; + GrepListView* fSearchResults; + + BMenuBar* fMenuBar; + BMenu* fFileMenu; + BMenuItem* fNew; + BMenuItem* fOpen; + BMenuItem* fClose; + BMenuItem* fAbout; + BMenuItem* fQuit; + BMenu* fActionMenu; + BMenuItem* fSelectAll; + BMenuItem* fSearch; + BMenuItem* fTrimSelection; + BMenuItem* fCopyText; + BMenuItem* fSelectInTracker; + BMenuItem* fOpenSelection; + BMenu* fPreferencesMenu; + BMenuItem* fRecurseLinks; + BMenuItem* fRecurseDirs; + BMenuItem* fSkipDotDirs; + BMenuItem* fCaseSensitive; + BMenuItem* fEscapeText; + BMenuItem* fTextOnly; + BMenuItem* fInvokePe; + BMenuItem* fShowLinesMenuitem; + BMenu* fHistoryMenu; + BMenu* fEncodingMenu; + BMenuItem* fUTF8; + BMenuItem* fShiftJIS; + BMenuItem* fEUC; + BMenuItem* fJIS; + + BCheckBox* fShowLinesCheckbox; + BButton* fButton; + + Grepper* fGrepper; + BString fOldPattern; + + Model* fModel; + + BFilePanel* fFilePanel; +}; + +#endif // GREP_WINDOW_H diff --git a/src/add-ons/tracker/text_search/Grepper.cpp b/src/add-ons/tracker/text_search/Grepper.cpp new file mode 100644 index 0000000000..b2e6cfb6a0 --- /dev/null +++ b/src/add-ons/tracker/text_search/Grepper.cpp @@ -0,0 +1,496 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "Grepper.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using std::nothrow; + +// TODO: stippi: Check if this is a the best place to maintain a global +// list of files and folders for node monitoring. It should probably monitor +// every file that was grepped, as well as every visited (sub) folder. +// For the moment I don't know the life cycle of the Grepper object. + + +char* +strdup_to_utf8(uint32 encode, const char* src, int32 length) +{ + int32 srcLen = length; + int32 dstLen = 2 * srcLen; + // TODO: stippi: Why the duplicate copy? Why not just return + // dst (and allocate with malloc() instead of new)? Is 2 * srcLen + // enough space? Check return value of convert_to_utf8 and keep + // converting if it didn't fit? + char* dst = new (nothrow) char[dstLen + 1]; + if (dst == NULL) + return NULL; + int32 cookie = 0; + convert_to_utf8(encode, src, &srcLen, dst, &dstLen, &cookie); + dst[dstLen] = '\0'; + char* dup = strdup(dst); + delete[] dst; + if (srcLen != length) { + fprintf(stderr, "strdup_to_utf8(%ld, %ld) dst allocate smalled(%ld)\n", + encode, length, dstLen); + } + return dup; +} + + +char* +strdup_from_utf8(uint32 encode, const char* src, int32 length) +{ + int32 srcLen = length; + int32 dstLen = srcLen; + char* dst = new (nothrow) char[dstLen + 1]; + if (dst == NULL) + return NULL; + int32 cookie = 0; + convert_from_utf8(encode, src, &srcLen, dst, &dstLen, &cookie); + // TODO: See above. + dst[dstLen] = '\0'; + char* dup = strdup(dst); + delete[] dst; + if (srcLen != length) { + fprintf(stderr, "strdup_from_utf8(%ld, %ld) dst allocate " + "smalled(%ld)\n", encode, length, dstLen); + } + return dup; +} + + +Grepper::Grepper(const char* pattern, Model* model) + : fDirectories(new (nothrow) BList(10)), + fCurrentDir(new (nothrow) BDirectory(&model->fDirectory)), + fCurrentRef(0), + fPattern(NULL), + fModel(model), + fThreadId(-1), + fMustQuit(false) +{ + if (!fCurrentDir || !fDirectories || !fDirectories->AddItem(fCurrentDir)) { + // init error + delete fCurrentDir; + fCurrentDir = NULL; + delete fDirectories; + fDirectories = NULL; + return; + } + + if (fModel->fEncoding) { + char *src = strdup_from_utf8(fModel->fEncoding, pattern, + strlen(pattern)); + _SetPattern(src); + free(src); + } else + _SetPattern(pattern); +} + + +Grepper::~Grepper() +{ + Cancel(); + + free(fPattern); + + // If the thread terminated normally, then there is only + // one object in the list: the initial directory. But if + // the user aborted the search, there may be more. + + if (fDirectories) { + for (int32 i = fDirectories->CountItems() - 1; i >= 0; i--) + delete static_cast(fDirectories->ItemAt(i)); + + delete fDirectories; + } +} + + +bool +Grepper::IsValid() const +{ + return fPattern != NULL && fDirectories != NULL && fCurrentDir != NULL; +} + + +void +Grepper::Start() +{ + Cancel(); + + fMustQuit = false; + fThreadId = spawn_thread( + _SpawnThread, "_GrepperThread", B_NORMAL_PRIORITY, this); + + resume_thread(fThreadId); +} + + +void +Grepper::Cancel() +{ + if (fThreadId < 0) + return; + + fMustQuit = true; + int32 exitValue; + wait_for_thread(fThreadId, &exitValue); + fThreadId = -1; +} + + +// #pragma mark - private + + +int32 +Grepper::_SpawnThread(void* cookie) +{ + Grepper* self = static_cast(cookie); + return self->_GrepperThread(); +} + + +int32 +Grepper::_GrepperThread() +{ + BMessage message; + + char fileName[B_PATH_NAME_LENGTH]; + char tempString[B_PATH_NAME_LENGTH]; + char command[B_PATH_NAME_LENGTH + 32]; + + BPath tempFile; + sprintf(fileName, "/boot/var/tmp/SearchText%ld", fThreadId); + tempFile.SetTo(fileName); + + while (!fMustQuit && _GetNextName(fileName)) { + message.MakeEmpty(); + message.what = MSG_REPORT_FILE_NAME; + message.AddString("filename", fileName); + fModel->fTarget->PostMessage(&message); + + message.MakeEmpty(); + message.what = MSG_REPORT_RESULT; + message.AddString("filename", fileName); + + BEntry entry(fileName); + entry_ref ref; + entry.GetRef(&ref); + message.AddRef("ref", &ref); + + if (!_EscapeSpecialChars(fileName, B_PATH_NAME_LENGTH)) { + sprintf(tempString, "%s: Not enough room to escape the filename.", + fileName); + + message.MakeEmpty(); + message.what = MSG_REPORT_ERROR; + message.AddString("error", tempString); + fModel->fTarget->PostMessage(&message); + continue; + } + + sprintf(command, "grep -hn %s %s \"%s\" > \"%s\"", + fModel->fCaseSensitive ? "" : "-i", fPattern, fileName, + tempFile.Path()); + + int res = system(command); + + if (res == 0 || res == 1) { + FILE *results = fopen(tempFile.Path(), "r"); + + if (results != NULL) { + while (fgets(tempString, B_PATH_NAME_LENGTH, results) != 0) { + if (fModel->fEncoding) { + char *tempdup = strdup_to_utf8(fModel->fEncoding, + tempString, strlen(tempString)); + message.AddString("text", tempdup); + free(tempdup); + } else + message.AddString("text", tempString); + } + + if (message.HasString("text")) + fModel->fTarget->PostMessage(&message); + + fclose(results); + continue; + } + } + + sprintf(tempString, "%s: There was a problem running grep.", fileName); + + message.MakeEmpty(); + message.what = MSG_REPORT_ERROR; + message.AddString("error", tempString); + fModel->fTarget->PostMessage(&message); + } + + // We wait with removing the temporary file until after the + // entire search has finished, to prevent a lot of flickering + // if the Tracker window for /boot/var/tmp/ might be open. + + remove(tempFile.Path()); + + message.MakeEmpty(); + message.what = MSG_SEARCH_FINISHED; + fModel->fTarget->PostMessage(&message); + + return 0; +} + + +void +Grepper::_SetPattern(const char* src) +{ + if (src == NULL) + return; + + if (!fModel->fEscapeText) { + fPattern = strdup(src); + return; + } + + // We will simply guess the size of the memory buffer + // that we need. This should always be large enough. + fPattern = (char*)malloc((strlen(src) + 1) * 3 * sizeof(char)); + if (fPattern == NULL) + return; + + const char* srcPtr = src; + char* dstPtr = fPattern; + + // Put double quotes around the pattern, so separate + // words are considered to be part of a single string. + *dstPtr++ = '"'; + + while (*srcPtr != '\0') { + char c = *srcPtr++; + + // Put a backslash in front of characters + // that should be escaped. + if ((c == '.') || (c == ',') + || (c == '[') || (c == ']') + || (c == '?') || (c == '*') + || (c == '+') || (c == '-') + || (c == ':') || (c == '^') + || (c == '\'') || (c == '"')) { + *dstPtr++ = '\\'; + } else if ((c == '\\') || (c == '$')) { + // Some characters need to be escaped + // with *three* backslashes in a row. + *dstPtr++ = '\\'; + *dstPtr++ = '\\'; + *dstPtr++ = '\\'; + } + + // Note: we do not have to escape the + // { } ( ) < > and | characters. + + *dstPtr++ = c; + } + + *dstPtr++ = '"'; + *dstPtr = '\0'; +} + + +bool +Grepper::_EscapeSpecialChars(char* buffer, ssize_t bufferSize) +{ + char* copy = strdup(buffer); + char* start = buffer; + uint32 len = strlen(copy); + bool result = true; + for (uint32 count = 0; count < len; ++count) { + if (copy[count] == '"' || copy[count] == '$') + *buffer++ = '\\'; + if (buffer - start == bufferSize - 1) { + result = false; + break; + } + *buffer++ = copy[count]; + } + *buffer = '\0'; + free(copy); + return result; +} + + +bool +Grepper::_GetNextName(char* buffer) +{ + BEntry entry; + struct stat fileStat; + + while (true) { + // Traverse the directory to get a new BEntry. + // _GetNextEntry returns false if there are no + // more entries, and we exit the loop. + + if (!_GetNextEntry(entry)) + return false; + + // If the entry is a subdir, then add it to the + // list of directories and continue the loop. + // If the entry is a file and we can grep it + // (i.e. it is a text file), then we're done + // here. Otherwise, continue with the next entry. + + if (entry.GetStat(&fileStat) == B_OK) { + if (S_ISDIR(fileStat.st_mode)) { + // subdir + _ExamineSubdir(entry); + } else { + // file or a (non-traversed) symbolic link + if (_ExamineFile(entry, buffer)) + return true; + } + } + } +} + + +bool +Grepper::_GetNextEntry(BEntry& entry) +{ + if (fDirectories->CountItems() == 1) + return _GetTopEntry(entry); + else + return _GetSubEntry(entry); +} + + +bool +Grepper::_GetTopEntry(BEntry& entry) +{ + // If the user selected one or more files, we must look + // at the "refs" inside the message that was passed into + // our add-on's process_refs(). If the user didn't select + // any files, we will simply read all the entries from the + // current working directory. + + entry_ref fileRef; + + if (fModel->fSelectedFiles.FindRef("refs", + fCurrentRef, &fileRef) == B_OK) { + entry.SetTo(&fileRef, fModel->fRecurseLinks); + ++fCurrentRef; + return true; + } else if (fCurrentRef > 0) { + // when we get here, we have processed + // all the refs from the message + return false; + } else { + // examine the whole directory + return fCurrentDir->GetNextEntry(&entry, + fModel->fRecurseLinks) == B_OK; + } +} + + +bool +Grepper::_GetSubEntry(BEntry& entry) +{ + if (!fCurrentDir) + return false; + + if (fCurrentDir->GetNextEntry(&entry, fModel->fRecurseLinks) == B_OK) + return true; + + // If we get here, there are no more entries in + // this subdir, so return to the parent directory. + + fDirectories->RemoveItem(fCurrentDir); + delete fCurrentDir; + fCurrentDir = (BDirectory*)fDirectories->LastItem(); + + return _GetNextEntry(entry); +} + + +void +Grepper::_ExamineSubdir(BEntry& entry) +{ + if (!fModel->fRecurseDirs) + return; + + if (fModel->fSkipDotDirs) { + char nameBuf[B_FILE_NAME_LENGTH]; + if (entry.GetName(nameBuf) == B_OK) { + if (*nameBuf == '.') + return; + } + } + + BDirectory* dir = new (nothrow) BDirectory(&entry); + if (dir == NULL || dir->InitCheck() != B_OK + || !fDirectories->AddItem(dir)) { + // clean up + delete dir; + return; + } + + fCurrentDir = dir; +} + + +bool +Grepper::_ExamineFile(BEntry& entry, char* buffer) +{ + BPath path; + if (entry.GetPath(&path) != B_OK) + return false; + + strcpy(buffer, path.Path()); + + if (!fModel->fTextOnly) + return true; + + BNode node(&entry); + BNodeInfo nodeInfo(&node); + char mimeTypeString[B_MIME_TYPE_LENGTH]; + + if (nodeInfo.GetType(mimeTypeString) == B_OK) { + BMimeType mimeType(mimeTypeString); + BMimeType superType; + + if (mimeType.GetSupertype(&superType) == B_OK) { + if (strcmp("text", superType.Type()) == 0 + || strcmp("message", superType.Type()) == 0) { + return true; + } + } + } + + return false; +} + diff --git a/src/add-ons/tracker/text_search/Grepper.h b/src/add-ons/tracker/text_search/Grepper.h new file mode 100644 index 0000000000..9043115be6 --- /dev/null +++ b/src/add-ons/tracker/text_search/Grepper.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef GREPPER_H +#define GREPPER_H + +#include "Model.h" + +// Executes "grep" in a background thread. +class Grepper { +public: + Grepper(const char* pattern, Model* model); + virtual ~Grepper(); + + bool IsValid() const; + + void Start(); + void Cancel(); + +private: + // Spawns the real grepper thread. + static int32 _SpawnThread(void* cookie); + + // The thread function that does the actual grepping. + int32 _GrepperThread(); + + // Remembers, and possibly escapes, the search pattern. + void _SetPattern(const char* source); + + // Prepends all quotes, dollars and backslashes with at backslash + // to prevent the shell from misinterpreting them. + bool _EscapeSpecialChars(char* buffer, + ssize_t bufferSize); + + // Returns the full path name of the next file. + bool _GetNextName(char* buffer); + + // Looks for the next entry. + bool _GetNextEntry(BEntry& entry); + + // Looks for the next entry in the top-level dir. + bool _GetTopEntry(BEntry& entry); + + // Looks for the next entry in a subdir. + bool _GetSubEntry(BEntry& entry); + + // Determines whether we can add a subdir. + void _ExamineSubdir(BEntry& entry); + + // Determines whether we can grep a file. + bool _ExamineFile(BEntry& entry, char* buffer); + +private: + // Contains pointers to BDirectory objects. + BList* fDirectories; + + // The directory we are currently looking at. + BDirectory* fCurrentDir; + + // The ref number we are currently looking at. + int32 fCurrentRef; + + // The (escaped) search pattern. + char* fPattern; + + // The directory or files to grep on. + Model* fModel; + + // Our thread's ID. + thread_id fThreadId; + + // Whether our thread must quit. + volatile bool fMustQuit; +}; + +#endif // GREPPER_H diff --git a/src/add-ons/tracker/text_search/Jamfile b/src/add-ons/tracker/text_search/Jamfile new file mode 100644 index 0000000000..52c8093ee7 --- /dev/null +++ b/src/add-ons/tracker/text_search/Jamfile @@ -0,0 +1,18 @@ +SubDir HAIKU_TOP src add-ons tracker text_search ; + +SetSubDirSupportedPlatformsBeOSCompatible ; + +# TODO: does not seem to work: +AddResources FileType-F : FileType.rdef ; + +Application TextSearch-G : + GrepApp.cpp + GrepListView.cpp + Grepper.cpp + GrepWindow.cpp + Model.cpp + TextSearch.cpp + + : be tracker textencoding + : TextSearch.rdef +; diff --git a/src/add-ons/tracker/text_search/Model.cpp b/src/add-ons/tracker/text_search/Model.cpp new file mode 100644 index 0000000000..13ed421f6d --- /dev/null +++ b/src/add-ons/tracker/text_search/Model.cpp @@ -0,0 +1,355 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "Model.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +using std::nothrow; + + +Model::Model() + : fDirectory(), + fSelectedFiles(0UL), + + fRecurseDirs(true), + fRecurseLinks(false), + fSkipDotDirs(true), + fCaseSensitive(false), + fEscapeText(true), + fTextOnly(true), + fInvokePe(false), + fShowContents(false), + + fFrame(100, 100, 500, 400), + + fTarget(NULL), + fState(STATE_IDLE), + + fFilePanelPath(""), + + fEncoding(0) +{ + BPath path; + status_t status = find_directory(B_USER_DIRECTORY, &path); + if (status == B_OK) + fFilePanelPath = path.Path(); + else + fFilePanelPath = "/boot/home"; +} + + +status_t +Model::LoadPrefs() +{ + BFile file; + status_t status = _OpenFile(&file, PREFS_FILE, B_READ_ONLY, + B_USER_SETTINGS_DIRECTORY, NULL); + + if (status != B_OK) + return status; + + status = file.Lock(); + if (status != B_OK) + return status; + + int32 value; + + if (file.ReadAttr("RecurseDirs", B_INT32_TYPE, 0, &value, + sizeof(int32)) > 0) + fRecurseDirs = (value != 0); + + if (file.ReadAttr("RecurseLinks", B_INT32_TYPE, 0, &value, + sizeof(int32)) > 0) + fRecurseLinks = (value != 0); + + if (file.ReadAttr("SkipDotDirs", B_INT32_TYPE, 0, &value, + sizeof(int32)) > 0) + fSkipDotDirs = (value != 0); + + if (file.ReadAttr("CaseSensitive", B_INT32_TYPE, 0, &value, + sizeof(int32)) > 0) + fCaseSensitive = (value != 0); + + if (file.ReadAttr("EscapeText", B_INT32_TYPE, 0, &value, + sizeof(int32)) > 0) + fEscapeText = (value != 0); + + if (file.ReadAttr("TextOnly", B_INT32_TYPE, 0, &value, + sizeof(int32)) > 0) + fTextOnly = (value != 0); + + if (file.ReadAttr("InvokePe", B_INT32_TYPE, 0, &value, + sizeof(int32)) > 0) + fInvokePe = (value != 0); + + if (file.ReadAttr("ShowContents", B_INT32_TYPE, 0, &value, + sizeof(int32)) > 0) + fShowContents = (value != 0); + + char buffer [B_PATH_NAME_LENGTH+1]; + int32 length = file.ReadAttr("FilePanelPath", B_STRING_TYPE, 0, &buffer, + sizeof(buffer)); + if (length > 0) { + buffer[length] = '\0'; + fFilePanelPath = buffer; + } + + file.ReadAttr("WindowFrame", B_RECT_TYPE, 0, &fFrame, sizeof(BRect)); + + if (file.ReadAttr("Encoding", B_INT32_TYPE, 0, &value, sizeof(int32)) > 0) + fEncoding = value; + + file.Unlock(); + + return B_OK; +} + + +status_t +Model::SavePrefs() +{ + BFile file; + status_t status = _OpenFile(&file, PREFS_FILE, + B_CREATE_FILE | B_WRITE_ONLY, B_USER_SETTINGS_DIRECTORY, NULL); + + if (status != B_OK) + return status; + + status = file.Lock(); + if (status != B_OK) + return status; + + int32 value = 2; + file.WriteAttr("Version", B_INT32_TYPE, 0, &value, sizeof(int32)); + + value = fRecurseDirs ? 1 : 0; + file.WriteAttr("RecurseDirs", B_INT32_TYPE, 0, &value, sizeof(int32)); + + value = fRecurseLinks ? 1 : 0; + file.WriteAttr("RecurseLinks", B_INT32_TYPE, 0, &value, sizeof(int32)); + + value = fSkipDotDirs ? 1 : 0; + file.WriteAttr("SkipDotDirs", B_INT32_TYPE, 0, &value, sizeof(int32)); + + value = fCaseSensitive ? 1 : 0; + file.WriteAttr("CaseSensitive", B_INT32_TYPE, 0, &value, sizeof(int32)); + + value = fEscapeText ? 1 : 0; + file.WriteAttr("EscapeText", B_INT32_TYPE, 0, &value, sizeof(int32)); + + value = fTextOnly ? 1 : 0; + file.WriteAttr("TextOnly", B_INT32_TYPE, 0, &value, sizeof(int32)); + + value = fInvokePe ? 1 : 0; + file.WriteAttr("InvokePe", B_INT32_TYPE, 0, &value, sizeof(int32)); + + value = fShowContents ? 1 : 0; + file.WriteAttr("ShowContents", B_INT32_TYPE, 0, &value, sizeof(int32)); + + file.WriteAttr("WindowFrame", B_RECT_TYPE, 0, &fFrame, sizeof(BRect)); + + file.WriteAttr("FilePanelPath", B_STRING_TYPE, 0, fFilePanelPath.String(), + fFilePanelPath.Length() + 1); + + file.WriteAttr("Encoding", B_INT32_TYPE, 0, &fEncoding, sizeof(int32)); + + file.Sync(); + file.Unlock(); + + return B_OK; +} + + +void +Model::AddToHistory(const char* text) +{ + BList* items = _LoadHistory(); + if (items == NULL) + return; + + BString* string = new (nothrow) BString(text); + if (string == NULL || !items->AddItem(string)) { + delete string; + return; + } + + int32 count = items->CountItems() - 1; + // don't check last item, since that's the one we just added + for (int32 t = 0; t < count; ++t) { + // If the same text is already in the list, + // then remove it first. Case-sensitive. + BString* string = static_cast(items->ItemAt(t)); + if (*string == text) { + delete static_cast(items->RemoveItem(t)); + break; + } + } + + if (items->CountItems() == HISTORY_LIMIT) + delete static_cast(items->RemoveItem(0L)); + + _SaveHistory(items); + _FreeHistory(items); +} + + +void +Model::FillHistoryMenu(BMenu* menu) +{ + BList* items = _LoadHistory(); + if (items == NULL) + return; + + for (int32 t = items->CountItems() - 1; t >= 0; --t) { + BString* item = static_cast(items->ItemAt(t)); + BMessage* message = new BMessage(MSG_SELECT_HISTORY); + message->AddString("text", item->String()); + menu->AddItem(new BMenuItem(item->String(), message)); + } + + _FreeHistory(items); +} + + +// #pragma mark - private + + +BList* +Model::_LoadHistory() +{ + BList* items = new (nothrow) BList(); + if (items == NULL) + return NULL; + + BFile file; + status_t status = _OpenFile(&file, PREFS_FILE, B_READ_ONLY, + B_USER_SETTINGS_DIRECTORY, NULL); + + if (status != B_OK) + return items; + + status = file.Lock(); + if (status != B_OK) + return items; + + BMessage message; + status = message.Unflatten(&file); + if (status != B_OK) + return items; + + file.Unlock(); + + BString string; + for (int32 x = 0; message.FindString("string", x, &string) == B_OK; x++) { + BString* copy = new (nothrow) BString(string); + if (copy == NULL || !items->AddItem(copy)) { + delete copy; + break; + } + } + + return items; +} + + +status_t +Model::_SaveHistory(BList* items) +{ + BFile file; + status_t status = _OpenFile(&file, PREFS_FILE, + B_CREATE_FILE | B_WRITE_ONLY, + B_USER_SETTINGS_DIRECTORY, NULL); + + if (status != B_OK) + return status; + + status = file.Lock(); + if (status != B_OK) + return status; + + BMessage message; + for (int32 x = 0; ; x++) { + BString* string = static_cast(items->ItemAt(x)); + if (string == NULL) + break; + + if (message.AddString("string", string->String()) != B_OK) + break; + } + + status = message.Flatten(&file); + file.SetSize(message.FlattenedSize()); + file.Sync(); + file.Unlock(); + + return status; +} + + +void +Model::_FreeHistory(BList* items) +{ + for (int32 t = items->CountItems() - 1; t >= 0; --t) + delete static_cast((items->RemoveItem(t))); + + delete items; +} + + +status_t +Model::_OpenFile(BFile* file, const char* name, uint32 openMode = B_READ_ONLY, + directory_which which = B_USER_SETTINGS_DIRECTORY, BVolume* volume = NULL) +{ + if (file == NULL) + return B_BAD_VALUE; + + BPath path; + status_t status = find_directory(which, &path, true, volume); + if (status != B_OK) + return status; + + status = path.Append(PREFS_FILE); + if (status != B_OK) + return status; + + status = file->SetTo(path.Path(), openMode); + if (status != B_OK) + return status; + + status = file->InitCheck(); + if (status != B_OK) + return status; + + return B_OK; +} diff --git a/src/add-ons/tracker/text_search/Model.h b/src/add-ons/tracker/text_search/Model.h new file mode 100644 index 0000000000..0fe76222be --- /dev/null +++ b/src/add-ons/tracker/text_search/Model.h @@ -0,0 +1,147 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef MODEL_H +#define MODEL_H + +#include +#include +#include +#include +#include +#include +#include +#include + + +#define PREFS_FILE "TrackerGrepSettings" +#define HISTORY_LIMIT 20 + +#define TRACKER_SIGNATURE "application/x-vnd.Be-TRAK" +#define PE_SIGNATURE "application/x-vnd.beunited.pe" + + +enum { + MSG_START_CANCEL = 1000, + MSG_RECURSE_LINKS, + MSG_RECURSE_DIRS, + MSG_SKIP_DOT_DIRS, + MSG_CASE_SENSITIVE, + MSG_ESCAPE_TEXT, + MSG_TEXT_ONLY, + MSG_INVOKE_PE, + MSG_MENU_SHOW_LINES, + MSG_CHECKBOX_SHOW_LINES, + MSG_SEARCH_TEXT, + MSG_INVOKE_ITEM, + MSG_SELECT_HISTORY, + + MSG_REPORT_FILE_NAME, + MSG_REPORT_RESULT, + MSG_REPORT_ERROR, + MSG_SEARCH_FINISHED, + + MSG_NEW_WINDOW, + MSG_OPEN_PANEL, + MSG_REFS_RECEIVED, + MSG_TRY_QUIT, + MSG_QUIT_NOW, + + MSG_TRIM_SELECTION, + MSG_COPY_TEXT, + MSG_SELECT_IN_TRACKER, + MSG_SELECT_ALL, + MSG_OPEN_SELECTION +}; + +enum state_t { + STATE_IDLE = 0, + STATE_SEARCH, + STATE_CANCEL +}; + +class Model { +public: + Model(); + + status_t LoadPrefs(); + status_t SavePrefs(); + + void AddToHistory(const char* text); + void FillHistoryMenu(BMenu* menu); + + // The directory we were invoked from. + entry_ref fDirectory; + + // The selected files we were invoked upon. + BMessage fSelectedFiles; + + // Whether we need to look into subdirectories. + bool fRecurseDirs; + + // Whether we need to follow symbolic links. + bool fRecurseLinks; + + // Whether we should skip subdirectories that start with a dot. + bool fSkipDotDirs; + + // Whether the search is case sensitive. + bool fCaseSensitive; + + // Whether the search pattern will be escaped. + bool fEscapeText; + + // Whether we look at text files only. + bool fTextOnly; + + // Whether we open the item in Pe and jump to the correct line. + bool fInvokePe; + + // Whether to show the contents of matching files. + bool fShowContents; + + // The dimensions of the window. + BRect fFrame; + + // The looper that will receive notifications. + BLooper* fTarget; + + // What are we doing. + state_t fState; + + // Current directory of the filepanel + BString fFilePanelPath; + + // Grep string encoding ? + uint32 fEncoding; + +private: + BList* _LoadHistory(); + status_t _SaveHistory(BList* items); + void _FreeHistory(BList* items); + status_t _OpenFile(BFile* file, const char* name, + uint32 openMode = B_READ_ONLY, + directory_which which + = B_USER_SETTINGS_DIRECTORY, + BVolume* volume = NULL); +}; + +#endif // MODEL_H diff --git a/src/add-ons/tracker/text_search/TextSearch.cpp b/src/add-ons/tracker/text_search/TextSearch.cpp new file mode 100644 index 0000000000..1a2b004adb --- /dev/null +++ b/src/add-ons/tracker/text_search/TextSearch.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 1998-2007 Matthijs Hollemans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "GrepApp.h" + +#include +#include +#include +#include + +#include "GlobalDefs.h" + + +int +main() +{ + GrepApp app; + app.Run(); + return 0; +} + + +void +process_refs(entry_ref dirRef, BMessage *message, void* /*reserved*/) +{ + // Tracker calls this function when the user invokes the add-on. + // "dir_ref" contains the entry_ref of the current directory. + // "message" is a standard B_REFS_RECEIVED BMessage whose "refs" + // array contains the entry_refs of the selected files. The last + // argument, "reserved", is currently unused. + + // This version of TrackerGrep is a Tracker add-on, but primarily + // it is a stand-alone application. The add-on launches the app + // on the set of files you had selected in Tracker. That way you + // get the benefits of the Tracker add-on with the benefits of the + // stand-alone application. + + message->AddRef("dir_ref", &dirRef); + + // get the path of the Tracker add-on + image_info image; + int32 cookie = 0; + status_t status = B_OK; + + status = get_next_image_info(0, &cookie, &image); + + while (status == B_OK) { + if (((char*)process_refs >= (char*)image.text + && (char*)process_refs <= (char*)image.text + image.text_size) + || ((char*)process_refs >= (char*)image.data + && (char*)process_refs <= (char*)image.data + image.data_size)) + break; + + status = get_next_image_info(0, &cookie, &image); + } + + entry_ref addonRef; + + if (get_ref_for_path(image.name, &addonRef) == B_OK) { + // It's better to launch the application by its entry + // than by its application signature. There may be + // multiple instances and we would not be certain + // that the desired one is launched - the one which was + // loaded into Tracker and whose process_refs() was called. + be_roster->Launch(&addonRef, message); + } else + be_roster->Launch(APP_SIGNATURE, message); +} diff --git a/src/add-ons/tracker/text_search/TextSearch.rdef b/src/add-ons/tracker/text_search/TextSearch.rdef new file mode 100644 index 0000000000..02345210a1 --- /dev/null +++ b/src/add-ons/tracker/text_search/TextSearch.rdef @@ -0,0 +1,46 @@ +/* + * TextSearch.rdef + */ + +resource app_signature "application/x-vnd.mahlzeit.trackergrep"; + +resource app_version { + major = 5, + middle = 1, + minor = 1, + + variety = B_APPV_FINAL, + internal = 0, + + short_info = "Context search using Grep", + long_info = "Context search in Tracker, a GUI wrapper of /bin/grep." +}; + +resource app_flags B_SINGLE_LAUNCH; + +resource file_types message { + "types" = "text", + "types" = "application/x-vnd.Be-directory", + "types" = "application/x-vnd.Be-symlink" +}; + +resource vector_icon { + $"6E6369660A0200060338D2F73CD163BF82B23B84A94B88504870C900FFEFA5BD" + $"FFFCC0FFFFF8900501020106023E49240000000000003CAAAA4940004A3000FF" + $"FFFCC07CF1B706040192020016023A55A6BAC2293F0DA33E958646C2EB47A1D6" + $"0001FF9E03010000020012023B98000000000000003C44004AD8004ADA000001" + $"0FFF0180020106023C00000000000000003C00004A30004A500000DCF4FFFF60" + $"94AA02001603360FF5B7B2B23AD6A4392A794ABC0B4AF035FF55C285005005FF" + $"0E0606AE0BB40BBF4D33C3AFB75DC173BDEFC607C13EC804CA28BD82C118B920" + $"C51BBB40BF07B8083AB6BC0605AE02B57D3EB9B9C3EFB7BB44BBB751BD75C936" + $"CA8EC1B1402F0A093B593D5BBFCDC93E455BC516C5F160465B435D4544510A04" + $"5A425E3F5A3D574008022E40BDB53308023142BE34BC0308023444C0E5BB5108" + $"023746C0D8BD7508023A48C270BDB508023D4A4CBD820802404CC408BFE60605" + $"7A024658565D5B5CCA15CB54CB0FCA5A584A540204423AC2BF3ABE583A384438" + $"BF2438C38B424EBE584EC2BF4E4C444CC38B4CBF240606BA0A4C51565B585AC9" + $"1CCA4EC983C9E959584F4E4B4D0E0A03020203000A0101011001178400040A02" + $"0101000A0101001001178400040A000100000A04040604080A1815FF01178100" + $"040A04040604080A18001501178200040A04030907051815FF01178100040A06" + $"010B000A05010D1001178400040A08010D000A05010C1001178400040A07010C" + $"000A09010C023C00000000000000003C000048B00048F000" +}; diff --git a/src/add-ons/tracker/text_search/Translation.h b/src/add-ons/tracker/text_search/Translation.h new file mode 100644 index 0000000000..0cd350fbc7 --- /dev/null +++ b/src/add-ons/tracker/text_search/Translation.h @@ -0,0 +1,15 @@ +/* + * Public domain, baby! + * + * Localization for Zeta users + * + */ + +#ifndef TRANSLATION_H +#define TRANSLATION_H + +#ifndef _T +# define _T(x) x +#endif + +#endif // TRANSLATION_H