diff --git a/.gitignore b/.gitignore index e0dda138fd..1c39f96dae 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ iio-widgets/include/iio-widgets/scopy-iio-widgets_export.h iioutil/include/iioutil/scopy-iioutil_export.h pluginbase/include/pluginbase/scopy-pluginbase_config.h pluginbase/include/pluginbase/scopy-pluginbase_export.h +gui/include/gui/style_properties.h +gui/include/gui/style_attributes.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a743ad4fee..a2ab673749 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,14 @@ message(STATUS "SCOPY_RESOURCE_FILES: " ${SCOPY_RESOURCE_FILES}) option(ENABLE_TRANSLATION "Enable translation" ON) include(ScopyTranslation) +find_package(Python3 COMPONENTS Interpreter) +execute_process(COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/style_generator.py ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} RESULT_VARIABLE ret) +if(NOT ret EQUAL "0") + message( FATAL_ERROR "Failed to generate style files! error: ${ret}") +else() + message("-- Generated style files") +endif() + if(ENABLE_TRANSLATION) generate_translations() qt_add_resources(SCOPY_RESOURCES ${CMAKE_BINARY_DIR}/translations.qrc) diff --git a/core/include/core/style.h b/core/include/core/style.h new file mode 100644 index 0000000000..bf1f45077d --- /dev/null +++ b/core/include/core/style.h @@ -0,0 +1,32 @@ +#ifndef STYLEREPOSITORY_H +#define STYLEREPOSITORY_H + +#include +#include "scopy-core_export.h" + +namespace scopy { +class SCOPY_CORE_EXPORT Style : public QObject +{ + Q_OBJECT +public: + Style(QObject *parent = nullptr); + ~Style(); + + void init(); + + static QString getAttribute(char *key); + static QColor getColor(char *key); + static int getDimension(char *key); + static void setStyle(QWidget *widget, char *style); + +protected: + void applyStyle(); + +private: + static QJsonDocument *m_json; + QString m_jsonPath; + QString m_qssPath; +}; +} // namespace scopy + +#endif // STYLEREPOSITORY_H diff --git a/core/src/style.cpp b/core/src/style.cpp new file mode 100644 index 0000000000..328f7d00aa --- /dev/null +++ b/core/src/style.cpp @@ -0,0 +1,54 @@ +#include "style.h" +#include "qcolor.h" +#include "qjsonobject.h" +#include "qwidget.h" + +#include +#include +#include + +using namespace scopy; + +QJsonDocument *Style::m_json{new QJsonDocument()}; + +Style::Style(QObject *parent) + : QObject(parent) + , m_jsonPath("style_variables.json") + , m_qssPath("style.qss") +{} + +Style::~Style() {} + +void Style::init() +{ + QFile file(m_jsonPath); + file.open(QIODevice::ReadOnly); + QByteArray data = file.readAll(); + file.close(); + m_json = new QJsonDocument(QJsonDocument::fromJson(data)); + + applyStyle(); +} + +void Style::setStyle(QWidget *widget, char *style) { widget->setProperty(style, true); } + +QString Style::getAttribute(char *key) { return m_json->object().value(key).toString(); } + +QColor Style::getColor(char *key) { return QColor(getAttribute(key)); } + +int Style::getDimension(char *key) { return getAttribute(key).toInt(); } + +void Style::applyStyle() +{ + QFile file(m_qssPath); + file.open(QIODevice::ReadOnly); + QString style = QString(file.readAll()); + file.close(); + + for(const QString &key : m_json->object().keys()) { + QJsonValue value = m_json->object().value(key); + style.replace("&" + key + "&", value.toString()); + } + + qApp->setStyleSheet(style); +} diff --git a/main.cpp b/main.cpp index e7d2d7fecc..a329589bf7 100644 --- a/main.cpp +++ b/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include using namespace scopy; @@ -101,8 +102,8 @@ int main(int argc, char *argv[]) printRuntimeEnvironmentInfo(); ApplicationRestarter restarter(QString::fromLocal8Bit(argv[0])); a.setWindowIcon(QIcon(":/gui/icon.ico")); - a.setStyle("Fusion"); - a.setStyleSheet(Util::loadStylesheetFromFile(":/gui/stylesheets/default.qss")); + Style().init(); + ScopyMainWindow w; w.show(); diff --git a/tools/style_generator.py b/tools/style_generator.py new file mode 100644 index 0000000000..359a0307cc --- /dev/null +++ b/tools/style_generator.py @@ -0,0 +1,179 @@ +import os +import sys + + +def create_namespace_structure(extension): + namespace_structure = {} + + for dirpath, dirnames, filenames in os.walk(style_folder): + qss_files = [f for f in filenames if f.endswith(extension)] + if qss_files: + relative_path = os.path.relpath(dirpath, style_folder) + namespace_structure[relative_path] = { + os.path.splitext(f)[0]: os.path.join(dirpath, f).replace('\\', '/') + for f in qss_files + } + + return namespace_structure + + +def replace_property(filepath, value): + search_text = "&&property&&" + + with open(filepath, 'r') as file: + data = file.read() + if search_text not in data: + return False + data = data.replace(search_text, value) + + with open(qss_path, 'a') as file: + file.write(data) + + return True + + +def generate_qss_variable(filepath, indent): + value = os.path.relpath(filepath, style_folder).replace("/", "_").replace(".qss", "") + if not replace_property(filepath, value): + return "" + + return f'{indent}const char *const ' + os.path.basename(filepath).replace(".qss", '') + f' = "{value}";' + + +def generate_namespace_code(namespace_structure, variable_func): + code_lines = [] + + def add_namespaces(folder, files, level): + indent = '\t' * level + code_lines.append(f'{indent}namespace {folder[level]} {{') + if level + 1 == len(folder): + for filename, filepath in files.items(): + code_lines.append(variable_func(filepath, f'{indent}\t')) + else: + subnamespace = {k: v for k, v in namespace_structure.items() if + k.startswith('/'.join(folder[:level + 1]) + '/')} + if subnamespace: + for subpath, subfiles in subnamespace.items(): + subpath_parts = subpath.split(os.sep) + add_namespaces(subpath_parts, subfiles, level + 1) + code_lines.append(f'{indent}}} // namespace {folder[level]}') + + root_parts = sorted(namespace_structure.keys(), key=lambda x: x.count(os.sep)) + added_namespaces = set() + + for root_path in root_parts: + path_parts = root_path.split(os.sep) + if path_parts[0] not in added_namespaces: + add_namespaces(path_parts, namespace_structure[root_path], 0) + added_namespaces.add(path_parts[0]) + + return '\n'.join(code_lines) + + +def write_header_file(output_file, namespace_code, name): + def_name = name.upper().replace(".", "_") + + with open(output_file, 'w') as f: + f.write("#ifndef " + def_name + "\n") + f.write("#define " + def_name + "\n") + f.write("#include \n\n") + f.write(namespace_code) + f.write("\n\n#endif // " + def_name + "\n") + + +def open_qss_file(): + open(qss_path, 'w') + +def append_variable(key, value): + with open(json_path, 'a') as file: + file.write("\t\"" + key + "\": " + value + ",\n") + + +def parse_json_file(filepath): + with open(filepath, 'r') as file: + content = file.read() + + content = content.strip() + if content[0] != '{' or content[-1] != '}': + raise ValueError(f"Invalid JSON format in file {filepath}") + + json_content = {} + content = content[1:-1].strip() # Remove the outer braces + items = content.split(',') + + for item in items: + key, value = item.split(':') + key = key.strip().strip('"') + formatted_key = os.path.relpath(filepath, style_folder).replace("/", "_").replace(".json", + "") + "_" + key + json_content[key] = formatted_key + append_variable(formatted_key, value) + + return json_content + + +def generate_json_variable(filepath, indent): + with open(filepath, 'r') as file: + if len(file.read()) == 0: + return '' + + json = parse_json_file(filepath) + + variable = "" + for key in json: + variable += indent + 'const char* ' + key + ' = "' + json[key] + '";\n' + + return variable[:-1] + + + +def open_json_file(): + with open(json_path, 'w') as file: + file.write("{\n") + + +def close_json_file(): + with open(json_path, 'r+') as file: + content = file.read() + last_comma_index = content.rfind(',') + if last_comma_index != -1: + content = content[:last_comma_index] + file.seek(0) + file.write(content) + file.truncate() + file.write("\n}\n") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Error") + sys.exit(1) + + source_folder = sys.argv[1] + build_folder = sys.argv[2] + style_folder = source_folder + "/gui/style" + + # qss + qss_header_name = "style_properties.h" + qss_header_path = source_folder + "/gui/include/gui/" + qss_header_name + qss_name = "style.qss" + qss_path = build_folder + "/" + qss_name + + open_qss_file() + qss_namespace_code = generate_namespace_code(create_namespace_structure("qss"), + generate_qss_variable) + write_header_file(qss_header_path, qss_namespace_code, qss_header_name) + + # json + json_header_name = "style_attributes.h" + json_header_path = source_folder + "/gui/include/gui/" + json_header_name + json_name = "style_attributes.json" + json_path = build_folder + "/" + json_name + + open_json_file() + json_namespace_code = generate_namespace_code(create_namespace_structure(".json"), + generate_json_variable) + write_header_file(json_header_path, json_namespace_code, json_header_name) + close_json_file() + + sys.exit(0)