/**************************************************************************** PmuppAdmin.cpp Author: Andreas Suter e-mail: andreas.suter@psi.ch *****************************************************************************/ /*************************************************************************** * Copyright (C) 2010-2025 by Andreas Suter * * andreas.suter@psi.ch * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ /** * @file PmuppAdmin.cpp * * @brief Implementation of mupp configuration and administration classes. * * This file implements the configuration management system for mupp, including: * - XML parsing of the mupp_startup.xml configuration file * - Recent file history management * - Plot appearance settings (colors and markers) * - Theme preferences for UI elements * - Automatic creation of default configuration files * * The implementation provides: * - PmuppColor: RGB color storage and manipulation * - PmuppAdminXMLParser: XML configuration file parsing * - PmuppAdmin: Main configuration manager with file search hierarchy * * Configuration file handling: * - Searches multiple standard locations for configuration * - Creates default configuration if none found * - Automatically saves recent file history on exit * - Validates configuration values (marker codes, RGB ranges) */ #include #include #include #include #include #include #include #include #include #include #include #include #include "PmuppAdmin.h" //-------------------------------------------------------------------------- // implementation of PmuppColor class //-------------------------------------------------------------------------- /** * @brief Default constructor for PmuppColor. * * Initializes a color with an "UnDef" name and invalid RGB values (-1). * Invalid values indicate the color has not been properly initialized. */ PmuppColor::PmuppColor() { fName = "UnDef"; fRed = -1; fGreen = -1; fBlue = -1; } //-------------------------------------------------------------------------- /** * @brief Sets the RGB color values with validation. * * Sets the red, green, and blue components of the color. Each component * is validated to be in the range [0, 255]. Values outside this range * are ignored and the corresponding component remains unchanged. * * @param r red component (0-255) * @param g green component (0-255) * @param b blue component (0-255) */ void PmuppColor::setRGB(const int r, const int g, const int b) { if ((r>=0) && (r<=255)) fRed = r; if ((g>=0) && (g<=255)) fGreen = g; if ((b>=0) && (b<=255)) fBlue = b; } //-------------------------------------------------------------------------- // implementation of PmuppAdminXMLParser class //-------------------------------------------------------------------------- /** * @brief Constructor for XML parser. * * Initializes the XML parser and attempts to open and parse the * specified configuration file. The parsed data is stored in the * provided PmuppAdmin object. * * @param fln path to the mupp_startup.xml configuration file * @param admin pointer to the PmuppAdmin instance to populate with parsed data */ PmuppAdminXMLParser::PmuppAdminXMLParser(const QString& fln, PmuppAdmin *admin) : fAdmin(admin) { fValid = false; fKeyWord = eEmpty; QFile file(fln); if (!file.open(QFile::ReadOnly | QFile::Text)) { // warning and create default - STILL MISSING } fValid = parse(&file); } //-------------------------------------------------------------------------- /** * @brief Parses the mupp startup XML file. * * This method uses Qt's QXmlStreamReader to parse the configuration file. * It processes each XML element sequentially, calling appropriate handler * methods (startElement, characters, endElement) based on the element type. * * The parser is event-driven and maintains state through the fKeyWord * enumeration to determine how to process element content. * * @param device QIODevice (typically QFile) containing the XML data * * @return true if parsing succeeds without errors, false otherwise */ bool PmuppAdminXMLParser::parse(QIODevice *device) { fXml.setDevice(device); bool expectChars = false; while (!fXml.atEnd()) { fXml.readNext(); if (fXml.isStartDocument()) { startDocument(); } else if (fXml.isStartElement()) { startElement(); expectChars = true; } else if (fXml.isCharacters() && expectChars) { characters(); } else if (fXml.isEndElement()) { endElement(); expectChars = false; } else if (fXml.isEndDocument()) { endDocument(); } } if (fXml.hasError()) { QString msg; msg = QString("%1 Line %2, column %3").arg(fXml.errorString()).arg(fXml.lineNumber()).arg(fXml.columnNumber()); QMessageBox::critical(0, "**ERROR**", msg, QMessageBox::Ok, QMessageBox::NoButton); return false; } return true; } //-------------------------------------------------------------------------- /** * @brief Handler called at the start of XML document parsing. * * This is invoked when the XML parser encounters the document start. * Currently performs no actions but provides a hook for future initialization. * * @return always returns true */ bool PmuppAdminXMLParser::startDocument() { // nothing to be done here for now return true; } //-------------------------------------------------------------------------- /** * @brief Handler called when a new XML element is encountered. * * This method is invoked when the parser encounters an opening XML tag. * It identifies the element type and sets the fKeyWord state accordingly, * which determines how the element's content will be processed in the * characters() method. * * Recognized elements: * - path_file_name: Recent file entry * - ignore_theme_auto_detection: Theme detection override * - dark_theme_icon_menu: Menu icon theme * - dark_theme_icon_toolbar: Toolbar icon theme * - marker: Plot marker definition * - color: RGB color definition * * @return always returns true */ bool PmuppAdminXMLParser::startElement() { QString qName = fXml.name().toString(); if (qName == "path_file_name") { fKeyWord = eRecentFile; } else if (qName == "ignore_theme_auto_detection") { fKeyWord = eIgnoreThemeAutoDetection; } else if (qName == "dark_theme_icon_menu") { fKeyWord = eDarkThemeIconsMenu; } else if (qName == "dark_theme_icon_toolbar") { fKeyWord = eDarkThemeIconsToolbar; } else if (qName == "marker") { fKeyWord = eMarker; } else if (qName == "color") { fKeyWord = eColor; } return true; } //-------------------------------------------------------------------------- /** * @brief Handler called when an XML closing tag is encountered. * * This method is invoked when the parser encounters a closing XML tag. * It resets the fKeyWord state to eEmpty, ending the current element's * context for content processing. * * @return always returns true */ bool PmuppAdminXMLParser::endElement() { fKeyWord = eEmpty; return true; } //-------------------------------------------------------------------------- /** * @brief Handler called to process XML element content. * * This method processes the text content between XML opening and closing * tags. The content interpretation depends on the current fKeyWord state * (set by startElement). * * Content processing by element type: * - eRecentFile: Adds file path to recent files list * - eIgnoreThemeAutoDetection: Sets theme auto-detection flag (yes/no/true/false) * - eDarkThemeIconsMenu: Sets menu icon theme preference * - eDarkThemeIconsToolbar: Sets toolbar icon theme preference * - eMarker: Parses marker definition (code[,size]) * - eColor: Parses RGB color definition (r,g,b[,name]) * * @return true if content is valid, false on parsing errors */ bool PmuppAdminXMLParser::characters() { QString str = fXml.text().toString(); if (str.isEmpty()) return true; bool ok; int ival, r, g, b; double dval; QString name(""); QStringList tok; switch (fKeyWord) { case eRecentFile: fAdmin->addRecentFile(QString(str.toLatin1()).trimmed()); break; case eIgnoreThemeAutoDetection: if ((str == "yes") || (str == "y") || (str == "1") || (str == "true")) fAdmin->setIgnoreThemeAutoDetection(true); else fAdmin->setIgnoreThemeAutoDetection(false); break; case eDarkThemeIconsMenu: if ((str == "yes") || (str == "y") || (str == "1") || (str == "true")) fAdmin->setThemeIconsMenu(true); else fAdmin->setThemeIconsMenu(false); break; case eDarkThemeIconsToolbar: if ((str == "yes") || (str == "y") || (str == "1") || (str == "true")) fAdmin->setThemeIconsToolbar(true); else fAdmin->setThemeIconsToolbar(false); break; case eMarker: tok = str.split(",", Qt::SkipEmptyParts); if ((tok.count() != 1) && (tok.count() != 2)) { return false; } ival = tok[0].toInt(&ok); if (!ok) return false; dval = 1.0; if (tok.count() == 2) { dval = tok[1].toDouble(&ok); if (!ok) return false; } fAdmin->setMarker(ival, dval); break; case eColor: tok = str.split(",", Qt::SkipEmptyParts); if ((tok.count() != 3) && (tok.count() != 4)) { return false; } ival = tok[0].toInt(&ok); if (!ok) return false; r = ival; ival = tok[1].toInt(&ok); if (!ok) return false; g = ival; ival = tok[2].toInt(&ok); if (!ok) return false; b = ival; if (tok.count() == 4) name = tok[3]; fAdmin->setColor(r, g, b, name); break; default: break; } return true; } //-------------------------------------------------------------------------- /** * @brief Handler called at the end of XML document parsing. * * This is invoked when the parser reaches the end of the XML document. * Currently performs no post-processing but provides a hook for future * finalization tasks such as path expansion or validation. * * @return always returns true */ bool PmuppAdminXMLParser::endDocument() { return true; } //-------------------------------------------------------------------------- // implementation of PmuppAdmin class //-------------------------------------------------------------------------- /** * @brief Constructor for PmuppAdmin. * * Initializes the administration object and searches for a configuration * file in multiple standard locations. The search proceeds in order: * 1. Current directory (./mupp_startup.xml) * 2. User home directory ($HOME/.musrfit/mupp/mupp_startup.xml) * 3. MUSRFITPATH directory ($MUSRFITPATH/mupp_startup.xml) * 4. ROOTSYS directory ($ROOTSYS/bin/mupp_startup.xml) * * If no configuration file is found, creates a default one in the user's * home directory using the embedded resource template. * * The constructor loads all configuration settings including recent files, * markers, colors, and theme preferences via the PmuppAdminXMLParser. */ PmuppAdmin::PmuppAdmin() : QObject() { // XML Parser part // 1st: check local directory QString path = QString("./"); QString fln = QString("mupp_startup.xml"); QString pathFln = path + fln; QProcessEnvironment procEnv = QProcessEnvironment::systemEnvironment(); if (!QFile::exists(pathFln)) { // 2nd: check $HOME/.musrfit/mupp/mupp_startup.xml path = procEnv.value("HOME", ""); pathFln = path + "/.musrfit/mupp/" + fln; if (!QFile::exists(pathFln)) { // 3rd: check $MUSRFITPATH/mupp_startup.xml path = procEnv.value("MUSRFITPATH", ""); pathFln = path + "/" + fln; if (!QFile::exists(pathFln)) { // 4th: check $ROOTSYS/bin/mupp_startup.xml path = procEnv.value("ROOTSYS", ""); pathFln = path + "/bin/" + fln; if (!QFile::exists(pathFln)) { // 5th: not found anywhere hence create it path = procEnv.value("HOME", ""); pathFln = path + "/.musrfit/mupp/" + fln; createMuppStartupFile(); } } } } if (QFile::exists(pathFln)) { // administration file present PmuppAdminXMLParser handler(pathFln, this); if (!handler.isValid()) { QMessageBox::critical(0, "**ERROR**", "Error parsing mupp_startup.xml settings file.\nProbably a few things will not work porperly.\nPlease fix this first.", QMessageBox::Ok, QMessageBox::NoButton); return; } } else { QMessageBox::critical(0, "**ERROR**", "Couldn't find the mupp_startup.xml settings file.\nProbably a few things will not work porperly.\nPlease fix this first.", QMessageBox::Ok, QMessageBox::NoButton); return; } } //-------------------------------------------------------------------------- /** * @brief Destructor for PmuppAdmin. * * Saves the current recent file list to the configuration file before * destruction. This ensures that the file history is preserved across * application sessions. */ PmuppAdmin::~PmuppAdmin() { saveRecentFiles(); } //-------------------------------------------------------------------------- /** * @brief Adds a file to the recent files list. * * Adds a file path to the front of the recent files ring buffer. If the * file is already in the list, it is not added again (no duplicates). * The list is limited to MAX_RECENT_FILES entries; older entries are * removed when the limit is exceeded. * * @param str the full path and filename to add to recent files */ void PmuppAdmin::addRecentFile(const QString str) { // check if file name is not already present for (int i=0; i MAX_RECENT_FILES) fRecentFile.resize(MAX_RECENT_FILES); } //-------------------------------------------------------------------------- /** * @brief Retrieves a recent file path by index. * * Returns the file path at the specified position in the recent files list. * Index 0 refers to the most recently accessed file. * * @param idx zero-based index into the recent files list (0 is most recent) * * @return the file path if index is valid, or an empty string otherwise */ QString PmuppAdmin::getRecentFile(int idx) { QString str(""); if ((idx >= 0) && (idx < fRecentFile.size())) str = fRecentFile[idx]; return str; } //-------------------------------------------------------------------------- /** * @brief Retrieves a marker definition by index. * * Returns the marker at the specified index in the marker list. * Markers are used to define the visual appearance of data points in plots. * * @param idx zero-based index into the marker list * * @return the marker definition if index is valid, or a default marker otherwise */ PmuppMarker PmuppAdmin::getMarker(int idx) { PmuppMarker marker; if (idx >= fMarker.size()) return marker; return fMarker[idx]; } //-------------------------------------------------------------------------- /** * @brief Retrieves a color by name. * * Searches for a color with the specified name and returns its RGB values. * If the color is not found, all RGB components are set to -1. * * @param name the name of the color to retrieve * @param r output parameter: red component (0-255, or -1 if not found) * @param g output parameter: green component (0-255, or -1 if not found) * @param b output parameter: blue component (0-255, or -1 if not found) */ void PmuppAdmin::getColor(QString name, int &r, int &g, int &b) { int idx=-1; for (int i=0; ifColor.size())) { r = -1; g = -1; b = -1; } else { fColor[idx].getRGB(r, g, b); } } //-------------------------------------------------------------------------- /** * @brief Adds a marker definition to the configuration. * * Creates a new marker with the specified code and size, validates it, * and adds it to the marker list. Marker codes must be in the range 1-49 * (following ROOT conventions). Invalid markers are rejected with a warning. * * @param marker the marker code (1-49, ROOT style marker codes) * @param size the marker size multiplier */ void PmuppAdmin::setMarker(const int marker, const double size) { PmuppMarker markerObj; // make sure marker is in proper range if ((marker<1) || (marker>49)) { QMessageBox::warning(0, "WARNING", QString("Found Marker (%1) not in the expected range.\nWill ignore it.").arg(marker)); return; } markerObj.setMarker(marker); markerObj.setMarkerSize(size); fMarker.push_back(markerObj); } //-------------------------------------------------------------------------- /** * @brief Adds a color definition to the configuration. * * Creates a new color with the specified RGB values and optional name, * validates it, and adds it to the color list. RGB values must be in * the range 0-255. Invalid colors are rejected with a warning. * * @param r red component (0-255) * @param g green component (0-255) * @param b blue component (0-255) * @param name optional name for the color (empty by default) */ void PmuppAdmin::setColor(const int r, const int g, const int b, QString name) { if (((r<0) || (r>255)) || ((g<0) || (g>255)) || ((b<0) || (b>255))) { QMessageBox::warning(0, "WARNING", QString("Found Color (%1,%2,%3) not in the expected range.\nWill ignore it.").arg(r).arg(g).arg(b)); return; } PmuppColor color; color.setName(name); color.setRGB(r,g,b); fColor.push_back(color); } //-------------------------------------------------------------------------- /** * @brief Saves the recent files list to the configuration file. * * Updates the mupp_startup.xml file with the current recent file list. * The method first searches for a local configuration file in the current * directory; if not found, it uses the user's home directory version * ($HOME/.musrfit/mupp/mupp_startup.xml). * * The implementation: * 1. Reads the entire configuration file into memory * 2. Removes all existing \ entries * 3. Inserts current recent files list * 4. Writes the updated configuration back to disk * * This preserves all other configuration settings while updating only * the recent file history. */ void PmuppAdmin::saveRecentFiles() { // check if mupp_startup.xml is present in the current directory, and if yes, use this file to // save the recent file names otherwise use the "master" mupp_startup.xml QString str(""); QString fln = QString("./mupp_startup.xml"); if (!QFile::exists(fln)) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); fln = QString("%1/.musrfit/mupp/mupp_startup.xml").arg(env.value("HOME")); } if (QFile::exists(fln)) { // administration file present QVector data; QFile file(fln); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { std::cerr << std::endl << ">> PmuppAdmin::saveRecentFile: **ERROR** Cannot open " << fln.toLatin1().data() << " for reading." << std::endl; return; } QTextStream fin(&file); while (!fin.atEnd()) { data.push_back(fin.readLine()); } file.close(); // remove from data for (QVector::iterator it = data.begin(); it != data.end(); ++it) { if (it->contains("")) { it = data.erase(it); --it; } } // add recent files int i; for (i=0; i")) break; } if (i == data.size()) { std::cerr << std::endl << ">> PmuppAdmin::saveRecentFile: **ERROR** " << fln.toLatin1().data() << " seems to be corrupt." << std::endl; return; } i++; for (int j=0; j"; data.insert(i++, str); } if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { std::cerr << std::endl << ">> PmuppAdmin::saveRecentFile: **ERROR** Cannot open " << fln.toLatin1().data() << " for reading." << std::endl; return; } fin.setDevice(&file); for (int i=0; i