From 42fd81138d21c5e613d1c51dbd379f038bba42a6 Mon Sep 17 00:00:00 2001 From: Vincent Reher Date: Mon, 12 Sep 2022 08:39:03 -0700 Subject: [PATCH] Encapsulate hidden file detection into a class that can be used to generalize and replace hard-coded "dotfile" detection. This starts to address issue # 273. Hidden files can be identified using one or more match strings, with matching governed either by wildcard (default) or regexp (optional). Matching is either case-sensitive (default) or case-INsensitive (optional). A user interface for inspecting and modifying match criteria is provided. Match criteria is passed from/to applications or views as TQString that starts with a single character: 'w' - match strings are wildcards, match is case-sensitive 'W' - match strings are wildcards, match is case-INsensitive 'r' - match strings are regexps, match is case-sensitive 'R' - match strings are regexps, match is case-INsensitive The remainder of the string is a '/'-separated list of one or more match strings. The TDEHiddenFileMatcher class is derived from a more general TDEStringMatcher class which is available to applications to conduct other string matching tasks. Signed-off-by: Vincent Reher --- tdecore/CMakeLists.txt | 3 +- tdecore/kdebug.areas | 1 + tdecore/tdestringmatcher-dialog.cpp | 234 ++++++++++++++++++++++++++++ tdecore/tdestringmatcher-dialog.h | 74 +++++++++ tdecore/tdestringmatcher.cpp | 128 +++++++++++++++ tdecore/tdestringmatcher.h | 150 ++++++++++++++++++ 6 files changed, 589 insertions(+), 1 deletion(-) create mode 100644 tdecore/tdestringmatcher-dialog.cpp create mode 100644 tdecore/tdestringmatcher-dialog.h create mode 100644 tdecore/tdestringmatcher.cpp create mode 100644 tdecore/tdestringmatcher.h diff --git a/tdecore/CMakeLists.txt b/tdecore/CMakeLists.txt index 22c3bbc88..22e6e795b 100644 --- a/tdecore/CMakeLists.txt +++ b/tdecore/CMakeLists.txt @@ -72,7 +72,7 @@ install( FILES kcalendarsystem.h kcalendarsystemfactory.h kmacroexpander.h kmanagerselection.h kmountpoint.h kuser.h klockfile.h kidna.h ktempdir.h kshell.h fixx11h.h kxerrorhandler.h - tdelibs_export.h kde_file.h ktimezones.h + tdelibs_export.h kde_file.h ktimezones.h tdestringmatcher.h ${CMAKE_CURRENT_BINARY_DIR}/kdemacros.h DESTINATION ${INCLUDE_INSTALL_DIR} ) @@ -135,6 +135,7 @@ set( ${target}_SRCS kprotocolinfo_tdecore.cpp kprotocolinfofactory.cpp kxerrorhandler.cpp kuser.cpp tdeconfigskeleton.cpp tdeconfigdialogmanager.cpp klockfile.cpp kqiodevicegzip_p.cpp ktimezones.cpp ksimpledirwatch.cpp + tdestringmatcher.cpp tdestringmatcher-dialog.cpp ) tde_add_library( ${target} SHARED AUTOMOC diff --git a/tdecore/kdebug.areas b/tdecore/kdebug.areas index 24c87f524..833fd8737 100644 --- a/tdecore/kdebug.areas +++ b/tdecore/kdebug.areas @@ -259,6 +259,7 @@ 7041 KTar 7042 KAr 7043 tdeio (bookmarks) +7077 HiddenFileDef # 71xx are for tdeioslaves 7101 tdeio_file diff --git a/tdecore/tdestringmatcher-dialog.cpp b/tdecore/tdestringmatcher-dialog.cpp new file mode 100644 index 000000000..a710b2f89 --- /dev/null +++ b/tdecore/tdestringmatcher-dialog.cpp @@ -0,0 +1,234 @@ +#include "tdestringmatcher-dialog.h" +#include "tdestringmatcher.h" + +#include "tdeglobal.h" +#include "tdelocale.h" +#include "kdebug.h" + +Customize_TDEStringMatcher_Dialog::Customize_TDEStringMatcher_Dialog( + TQStringList initPatternList, + bool initPatternIsRegex, + bool initPatternIsCaseInsensitive, + TQString callerTitle +) +{ + // Initialize our "output" variables + patternList.clear(); + patternIsRegex = false; + patternIsCaseInsensitive = false; + dialogResult = CriteriaApplied; + + setModal( TRUE ); + setCaption( i18n( "Specify string match criteria" ) ); + + // Create main layout and subordinate grid to position the widgets + + TQHBoxLayout *topLayout = new TQHBoxLayout( this, 10 ); + TQGridLayout *grid = new TQGridLayout( 0, 2 ); // 2 wide and autodetect number of rows + topLayout->addLayout( grid, 1 ); + int gridrow = 1; + + //=== Set up dialog title ===// + +//TQString titleGeneral = i18n( "Define 'Hidden' files for" ) ; +//setCaption( titleGeneral + " " + callerTitle ); + TQLabel *dialogTitle = new TQLabel( this ); + dialogTitle->setText( "" + callerTitle ); + dialogTitle->setFrameStyle( TQFrame::Box | TQFrame::Plain ); + dialogTitle->setAlignment( TQt::AlignHCenter | TQt::AlignVCenter ); + grid->addMultiCellWidget( dialogTitle, gridrow, gridrow, 0, 1 ); + gridrow++; + + //=== Set up match pattern edit boxes ===/ + + TQLabel *patterns_label = new TQLabel( this ); + patterns_label->setText( i18n( "Match any of these patterns" ) ); + patterns_label->setAlignment( TQt::AlignHCenter ); + grid->addMultiCellWidget( patterns_label, gridrow, gridrow, 0, 1 ); + gridrow++; + + // Limit the number of patterns to display to something reasonable + maxPatterns = initPatternList.size() + 2; + if ( maxPatterns > MAX_PATTERNS ) + maxPatterns = MAX_PATTERNS; + + // Set up edit boxes with initial patterns + short i = 0 ; + for ( + TQStringList::iterator it = initPatternList.begin(); + i < maxPatterns, it != initPatternList.end() ; + ++it, i++, gridrow++ + ) { + le_Patterns[i] = new TQLineEdit( this ); + le_Patterns[i]->setText(it->latin1()) ; + grid->addMultiCellWidget( le_Patterns[i], gridrow, gridrow, 0, 1 ); + } + short first_empty_editbox = i; + for ( ; i < maxPatterns; i++, gridrow++ ) { + le_Patterns[i] = new TQLineEdit( this ); + grid->addMultiCellWidget( le_Patterns[i], gridrow, gridrow, 0, 1 ); + } + le_Patterns[first_empty_editbox]->setFocus(); + + //=== Set up match option check boxes ===// + + // Create a group of check boxes + bg_Options = new TQButtonGroup( this, "checkGroup" ); + bg_Options->setTitle( i18n( "Match options" ) ); + grid->addMultiCellWidget( bg_Options, gridrow, gridrow, 0, 1 ); + gridrow++; + + // Create a layout for the check boxes + TQVBoxLayout *vbox = new TQVBoxLayout(bg_Options, 10); + vbox->addSpacing( bg_Options->fontMetrics().height() ); + + // Set up first checkbox + cb_patternIsRegex = new TQCheckBox( bg_Options ); + cb_patternIsRegex->setChecked( initPatternIsRegex ); + cb_patternIsRegex->setText( i18n( "&Regex (vs Wildcard)" ) ); + TQToolTip::add( cb_patternIsRegex, i18n( "Regex (vs Wildcard)" ) ); + vbox->addWidget( cb_patternIsRegex ); + + // Set up second checkbox + cb_patternIsCaseInsensitive = new TQCheckBox( bg_Options ); + cb_patternIsCaseInsensitive->setChecked( initPatternIsCaseInsensitive ); + cb_patternIsCaseInsensitive->setText( i18n( "&Case insensitive" ) ); + TQToolTip::add( cb_patternIsCaseInsensitive, i18n( "Case insensitive" ) ); + vbox->addWidget( cb_patternIsCaseInsensitive ); + + //=== Set up final dispostion radio buttons ===// + + // Create a group of radio buttons + bg_Disposition = new TQButtonGroup( this, "radioGroup" ); + bg_Disposition->setTitle( i18n( "Disposition of Match Criteria:" ) ); + bg_Disposition->setRadioButtonExclusive(true); + grid->addMultiCellWidget( bg_Disposition, gridrow, gridrow, 0, 1 ); + gridrow++; + + // Create a layout for the radio buttons + TQVBoxLayout *vbox2 = new TQVBoxLayout(bg_Disposition, 10); + vbox2->addSpacing( bg_Disposition->fontMetrics().height() ); + + // Set up radio button 0 + rb_Disposition = new TQRadioButton( bg_Disposition ); + rb_Disposition->setText( i18n( "&Apply but do not save" ) ); + rb_Disposition->setChecked( TRUE ); + TQToolTip::add( rb_Disposition, i18n( "Changes will be applied temporarily" ) ); + vbox2->addWidget( rb_Disposition ); + + // Set up radio button 1 + rb_Disposition = new TQRadioButton( bg_Disposition ); + rb_Disposition->setText( i18n( "Apply and &save as default" ) ); + TQToolTip::add( rb_Disposition, i18n( "Changes will be applied and saved to disk" ) ); + vbox2->addWidget( rb_Disposition ); + + // Set up radio button 2 + rb_Disposition = new TQRadioButton( bg_Disposition ); + rb_Disposition->setText( i18n( "Ignore and restore &default" ) ); + TQToolTip::add( rb_Disposition, i18n( "Changes will be ignored and saved default restored" ) ); + vbox2->addWidget( rb_Disposition ); + + connect( bg_Disposition, SIGNAL(clicked(int)), SLOT(setExitDisposition(int)) ); + + //=== Set up dialog exit buttons ===/ + + pb_OK = new TQPushButton( this, "pb_OK" ); + pb_OK->setDefault( FALSE ); + pb_OK->setText( i18n( "&OK" ) ); + TQToolTip::add( pb_OK, i18n( "Save settings as default" ) ); + pb_OK->setAccel( TQKeySequence( "Return" ) ); + + grid->addWidget( pb_OK, gridrow, 0 ); + + pb_Cancel = new TQPushButton( this, "pb_Cancel" ); + pb_Cancel->setDefault( FALSE ); + pb_Cancel->setText( i18n( "&Cancel" ) ); + pb_Cancel->setAccel( TQKeySequence( "Esc" ) ); + + grid->addWidget( pb_Cancel, gridrow, 1 ); + + connect( pb_OK, SIGNAL( clicked() ), this, SLOT( exitOK() ) ); + connect( pb_Cancel, SIGNAL( clicked() ), this, SLOT( exitCancel() ) ); +} + +//=== Dialog event handlers ===/ + +bool Customize_TDEStringMatcher_Dialog::event( TQEvent * e ) { + if ( e->type() != TQEvent::KeyPress ) { + TQKeyEvent * ke = (TQKeyEvent*) e; + if ( ke->key() == Key_Escape ) { + ke->accept(); + exitCancel(); + return TRUE; + } + else if ( ke->key() == Key_Return ) { + ke->accept(); + exitOK(); + return TRUE; + } + } + return TQWidget::event( e ); +} + +void Customize_TDEStringMatcher_Dialog::setExitDisposition(int buttonNum ) +{ + switch( buttonNum ) { + case 0: + dialogResult = CriteriaApplied; + break; + case 1: + dialogResult = SaveCriteria; + break; + case 2: + dialogResult = ReloadCriteria; + break; + } +} + +//=== Dialog exit functions ===// + +void Customize_TDEStringMatcher_Dialog::exitOK() +{ + if ( dialogResult > CriteriaUnchanged ) { + patternList.clear(); + for ( int i = 0; i < maxPatterns; i++ ) { + TQString pattern = le_Patterns[i]->text(); + if ( ! ( pattern.isEmpty() ) ) { + patternList += pattern; + } + } + patternIsRegex = cb_patternIsRegex->isChecked(); + patternIsCaseInsensitive = cb_patternIsCaseInsensitive->isChecked(); + } + close(); +} + +void Customize_TDEStringMatcher_Dialog::exitCancel() +{ + dialogResult = CriteriaUnchanged; + close(); +} + +//=== Object property accessors ===// + +int Customize_TDEStringMatcher_Dialog::result() +{ + return dialogResult; +} + +bool Customize_TDEStringMatcher_Dialog::isPatternRegex() +{ + return patternIsRegex; +} + +bool Customize_TDEStringMatcher_Dialog::isPatternCaseInsensitive() +{ + return patternIsCaseInsensitive; +} + +TQStringList Customize_TDEStringMatcher_Dialog::getPatternList() +{ + return patternList; +} + +#include "tdestringmatcher-dialog.moc" diff --git a/tdecore/tdestringmatcher-dialog.h b/tdecore/tdestringmatcher-dialog.h new file mode 100644 index 000000000..e2db5c134 --- /dev/null +++ b/tdecore/tdestringmatcher-dialog.h @@ -0,0 +1,74 @@ +#ifndef HIDDEN_FILE_DIALOG_H +#define HIDDEN_FILE_DIALOG_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tdeglobal.h" + +class TDECORE_EXPORT Customize_TDEStringMatcher_Dialog : public TQDialog +{ + Q_OBJECT + +public: + + Customize_TDEStringMatcher_Dialog ( + TQStringList initPatternList, + bool initPatternIsRegex, + bool initPatternIsCaseInsensitive, + TQString dialogTitle = TQString("Define String Matching Criteria") + ); + + int result(); + + TQStringList getPatternList(); + bool isPatternRegex(); + bool isPatternCaseInsensitive(); + +protected: + + bool event( TQEvent * e ); + +private slots: + + void setExitDisposition(int buttonNum ); + void exitOK(); + void exitCancel(); + +private: + + TQLabel *patterns_label; + #define MAX_PATTERNS 10 + TQLineEdit *le_Patterns[MAX_PATTERNS] ; + + TQButtonGroup *bg_Options; + TQCheckBox *cb_patternIsRegex; + TQCheckBox *cb_patternIsCaseInsensitive; + + TQButtonGroup *bg_Disposition; + TQRadioButton *rb_Disposition; + + TQPushButton *pb_OK; + TQPushButton *pb_Apply; + TQPushButton *pb_Reload; + TQPushButton *pb_Cancel; + + unsigned short maxPatterns ; + TQStringList patternList; + bool patternIsRegex; + bool patternIsCaseInsensitive; + + int dialogResult; + +}; + +#endif diff --git a/tdecore/tdestringmatcher.cpp b/tdecore/tdestringmatcher.cpp new file mode 100644 index 000000000..5d5005e62 --- /dev/null +++ b/tdecore/tdestringmatcher.cpp @@ -0,0 +1,128 @@ +#include "tdestringmatcher.h" +#include "tdestringmatcher-dialog.h" + +#include +#include "tdeconfig.h" +#include "kdebug.h" + +TDEStringMatcher::TDEStringMatcher() +{ + patternsDivider = patternsDividerDefault; + setCriteria( criteriaMatchNothing ); +} + +TDEStringMatcher::~TDEStringMatcher() +{ +} + +bool TDEStringMatcher::match( const TQString& filename ) +{ + TQRegExp rxMatcher; + if ( filename.isEmpty() ) + return FALSE; + rxMatcher.setWildcard( ! isRegex ); + rxMatcher.setCaseSensitive( ! isCaseInsensitive ); + for ( TQStringList::iterator it = patternList.begin(); it != patternList.end() ; ++it ) + { + rxMatcher.setPattern( *it ); + if ( rxMatcher.exactMatch( filename ) ) { + return( TRUE ) ; + } + } + return FALSE; +} + +TQString TDEStringMatcher::getCriteria() +{ + return Criteria; +} + +void TDEStringMatcher::generateCriteria() +{ + TQString newCriteria = patternList.join( TQString ( patternsDivider ) ) ; + TQChar leadingChar; + if ( isRegex ) + leadingChar = 'r'; + else + leadingChar = 'w' ; + if ( isCaseInsensitive ) + leadingChar = leadingChar.upper(); + newCriteria.prepend( leadingChar ); + Criteria = newCriteria; +} + +bool TDEStringMatcher::setCriteria( TQString newCriteria ) +{ + if ( newCriteria.length() == 0 ) { + return TRUE; // We always succeed when asked to do nothing! + } + + TQChar optionsChar = newCriteria[0]; + switch( optionsChar ) { + case 'w': isRegex = FALSE; isCaseInsensitive = FALSE; break; + case 'W': isRegex = FALSE; isCaseInsensitive = TRUE; break; + case 'r': isRegex = TRUE; isCaseInsensitive = FALSE; break; + case 'R': isRegex = TRUE; isCaseInsensitive = TRUE; break; + default : + kdWarning(7077) + << "TDEStringMatcher::setCriteria: invalid leading character in criteria string: '" + << newCriteria << "'" << endl; + return FALSE; + }; + + patternList.clear(); + patternList = TQStringList::split( patternsDivider, newCriteria.mid(1), TRUE ); + Criteria = newCriteria; + return TRUE; +} + +int TDEStringMatcher::getMatchPropertiesFromUser( TQString dialogTitle ) +{ + Customize_TDEStringMatcher_Dialog defineHiddenUI = + Customize_TDEStringMatcher_Dialog( patternList, isRegex, isCaseInsensitive, dialogTitle ); + + defineHiddenUI.exec(); + if ( defineHiddenUI.result() > CriteriaUnchanged ) { + patternList = defineHiddenUI.getPatternList(); + isRegex = defineHiddenUI.isPatternRegex(); + isCaseInsensitive = defineHiddenUI.isPatternCaseInsensitive(); + generateCriteria(); + kdDebug(7077) + << "TDEStringMatcher::getMatchPropertiesFromUser: updated criteria string: '" + << Criteria << "'" << endl; + } + return defineHiddenUI.result(); +} + +void TDEStringMatcher::setPatternsDivider( TQChar newPatternsDivider ) +{ + patternsDivider = newPatternsDivider; +} + + + +TDEHiddenFileMatcher::TDEHiddenFileMatcher() +{ + setPatternsDivider( patternsDivider4fileNames ); + setCriteria( getGlobalCriteria() ); +} + +TDEHiddenFileMatcher::~TDEHiddenFileMatcher() +{ +} + +TQString TDEHiddenFileMatcher::getGlobalCriteria() +{ + TDEConfig *config = TDEGlobal::config(); + TDEConfigGroupSaver saver( config, globalSettingsGroup ); + TQString defaultCriteria = config->readEntry( globalSettingsKey, criteriaMatchDotfiles ); + return defaultCriteria; +} + +void TDEHiddenFileMatcher::setGlobalCriteria() +{ + TDEConfig *config = TDEGlobal::config(); + TDEConfigGroupSaver saver( config, globalSettingsGroup ); + config->writeEntry( globalSettingsKey, Criteria ); +} + diff --git a/tdecore/tdestringmatcher.h b/tdecore/tdestringmatcher.h new file mode 100644 index 000000000..bcd2b08f8 --- /dev/null +++ b/tdecore/tdestringmatcher.h @@ -0,0 +1,150 @@ +#ifndef TDESTRINGMATCHER_H +#define TDESTRINGMATCHER_H + +#include +#include + +#include "tdeglobal.h" + +/************ + * + * Generic string matcher class. + */ +class TDECORE_EXPORT TDEStringMatcher +{ +public: + + TDEStringMatcher(); + ~TDEStringMatcher(); + + /** + Perform string matching based on the match-controlling + @properties described below. + */ + bool match( const TQString& ); + + /** + @return value of @property Criteria described below. + */ + TQString getCriteria(); + + /** + Process @param newCriteria to initialize / update the + @property patternsDivider described below. + */ + bool setCriteria(TQString newCriteria); + + /** + Present dialog that allows user to view/update current match- + controlling @properties described below. @return one of the + values of @enum UpdateResult listed below. @param dialogTitle + can be used to describe the application/ the software component that is using + the TDEStringMatcher. Examples: + "Hidden File Definitionfor Konqueror Listview" + "Global Default Hidden File Definition" + */ + int getMatchPropertiesFromUser( TQString dialogTitle = TQString("Define String Matching Criteria") ); + + enum class Status : int { + reloadCriteria = -1, // Criteria was not changed, request reloading default + criteriaUnchanged = 0, // Criteria was not changed + criteriaApplied = 1, // New criteria applied, should not be saved + saveCriteria = 2 // New criteria applied, request saving as default + }; + #define ReloadCriteria -1 + #define CriteriaUnchanged 0 + #define CriteriaApplied 1 + #define SaveCriteria 2 + + /** + Process @param newPatternsDivider to initialize / update + the match-controlling @properties that are described below. + */ + void setPatternsDivider(TQChar newPatternsDivider); + +protected: + + /** + Derive value of @property Criteria from the current values + of the match-controlling @properties described below + */ + void generateCriteria(); + + /** + Match-controlling properties: + + @property patternList is a list of zero or more string patterns each + of which will be used as a wildcard or regex to match a string. If + the list is empty, then no strings will match. + @property isCaseInsensitive determines whether or not matching + will be done in "case insensitive" fashion. + @property isRegex determines whether or not the matching will + be regex-based (versus wildcard/globbing-based). + + */ + bool isCaseInsensitive = FALSE; + bool isRegex = FALSE; + TQStringList patternList; // FIXME: Use STL? + + /** + @property patternsDivider is a character used to separate multiple + match patterns in @property Criteria described below. This should be + a character that will not be used in a match pattern. + */ + TQChar patternsDivider; + + /** + @property Criteria is a string which encodes the match-controlling + @properties described above. It has the format ^([wWrR])(.+). The + leading character's case determines @property isCaseInsensitive and + its value determines @property isRegex (rR: TRUE, wW: FALSE). + Remainder of string is a @property patternsDivider separated list of + match patterns that determines the content of @property patternList. + + */ + TQString Criteria; + +private: + + static constexpr const char patternsDividerDefault = ','; + static constexpr const char * criteriaMatchNothing = "w"; +}; + +/************ + * + * Hidden file matcher class. + */ + +class TDECORE_EXPORT TDEHiddenFileMatcher : public TDEStringMatcher { + friend class TDEGlobal; // for initInstance() + +public: + + TDEHiddenFileMatcher(); + ~TDEHiddenFileMatcher(); + +private: + + TQString getGlobalCriteria(); + void setGlobalCriteria(); + + /** + @property patternsDivider4fileNames specifies a reasonable + default for separating multiple file name match patterns. + */ + static constexpr const char patternsDivider4fileNames = '/'; + + /** + @property criteriaMatchDotfiles encodes the traditional + unix-style hidden file matching of so-called dotfiles. + It is default value of @property Criteria unless overridden. + */ + static constexpr const char * criteriaMatchDotfiles = "w.*"; + + // Default Criteria setting location in kdeglobals + static constexpr const char * globalSettingsGroup = "General"; + static constexpr const char * globalSettingsKey = "globalHiddenFileSpec"; + +}; + +#endif