diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..00a786d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## v1.0.0 + +- Initial release. Enjoy! diff --git a/CMakeLists.txt b/CMakeLists.txt index ae104ba..bb53cc1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,16 @@ project("qlementine" set(PROJECT_COPYRIGHT "© Olivier Cléro, MIT License.") set(PROJECT_NAMESPACE "oclero") +# Temporary hack to make it work with Qt6 <6.4.2 && >6.4.2 With Qt5, it was +# "path/to/Qt/6.7.0/msvc2019_64/lib/cmake/Qt6", but with Qt6, it is now +# "path/to/Qt/6.7.0/msvc2019_64". +if(WIN32) + string(FIND "${CMAKE_PREFIX_PATH}" "/lib/cmake/Qt6" USING_Qt6_INDEX) + if(NOT ${USING_Qt6_INDEX} EQUAL -1) + string(REPLACE "/lib/cmake/Qt6" "" "${CMAKE_PREFIX_PATH}" "${CMAKE_PREFIX_PATH}") + endif() +endif() + # Global flags. set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 17) @@ -23,7 +33,7 @@ set(CMAKE_AUTORCC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set_property(GLOBAL PROPERTY USE_FOLDERS ON) -if (NOT CMAKE_OSX_DEPLOYMENT_TARGET) +if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) set(CMAKE_OSX_DEPLOYMENT_TARGET "13.6") endif() @@ -31,12 +41,14 @@ endif() find_package(Qt6 REQUIRED COMPONENTS Core Widgets Svg) qt_standard_project_setup() -# include(DeployQt) - # The library. add_subdirectory(lib) -# Sandbox. -if(${PROJECT_IS_TOP_LEVEL}) +# Example apps using the lib. +if(QLEMENTINE_SANDBOX) add_subdirectory(sandbox) endif() + +if(QLEMENTINE_SHOWCASE) + add_subdirectory(showcase) +endif() diff --git a/CMakePresets.json b/CMakePresets.json index 7e24687..d18512e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -13,7 +13,9 @@ "generator": "Xcode", "binaryDir": "${sourceDir}/_build", "cacheVariables": { - "CMAKE_PREFIX_PATH": "/opt/homebrew/opt/qt/lib/cmake/Qt6" + "CMAKE_PREFIX_PATH": "/opt/homebrew/opt/qt/lib/cmake/Qt6", + "QLEMENTINE_SANDBOX": true, + "QLEMENTINE_SHOWCASE": true }, "condition": { "type": "equals", @@ -28,7 +30,9 @@ "generator": "Visual Studio 17 2022", "binaryDir": "${sourceDir}/_build", "cacheVariables": { - "CMAKE_PREFIX_PATH": "C:/Qt/6.8.0/msvc2022_64" + "CMAKE_PREFIX_PATH": "C:/Qt/6.8.0/msvc2022_64", + "QLEMENTINE_SANDBOX": true, + "QLEMENTINE_SHOWCASE": true }, "condition": { "type": "equals", @@ -42,6 +46,10 @@ "description": "Makefile for Linux", "generator": "Unix Makefiles", "binaryDir": "${sourceDir}/_build", + "cacheVariables": { + "QLEMENTINE_SANDBOX": true, + "QLEMENTINE_SHOWCASE": true + }, "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -54,9 +62,8 @@ "name": "macos", "displayName": "macOS", "configurePreset": "macos", - "description": "Release build with Xcode for macOS", + "description": "Build with Xcode for macOS", "targets": ["qlementine"], - "configuration": "Release", "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -67,9 +74,20 @@ "name": "macos-sandbox", "displayName": "Sandbox for macOS", "configurePreset": "macos", - "description": "Sandbox - Release build with Xcode for macOS", + "description": "Sandbox - Build with Xcode for macOS", "targets": ["sandbox"], - "configuration": "Release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos-showcase", + "displayName": "Showcase for macOS", + "configurePreset": "macos", + "description": "Showcase - Build with Xcode for macOS", + "targets": ["showcase"], "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -80,9 +98,8 @@ "name": "windows", "displayName": "Windows", "configurePreset": "windows", - "description": "Release build with Visual Studio for Windows", + "description": "Build with Visual Studio for Windows", "targets": ["qlementine"], - "configuration": "Release", "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -93,9 +110,20 @@ "name": "windows-sandbox", "displayName": "Sandbox for Windows", "configurePreset": "windows", - "description": "Sandbox - Release build with Visual Studio for Windows", + "description": "Sandbox - Build with Visual Studio for Windows", "targets": ["sandbox"], - "configuration": "Release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows-showcase", + "displayName": "Showcase for Windows", + "configurePreset": "windows", + "description": "Showcase - Build with Visual Studio for Windows", + "targets": ["showcase"], "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -106,9 +134,8 @@ "name": "linux", "displayName": "Linux", "configurePreset": "linux", - "description": "Release build for Linux", + "description": "Build for Linux", "targets": ["qlementine"], - "configuration": "Release", "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -119,9 +146,20 @@ "name": "linux-sandbox", "displayName": "Sandbox for Linux", "configurePreset": "linux", - "description": "Sandbox - Release build for Linux", + "description": "Sandbox - Build for Linux", "targets": ["sandbox"], - "configuration": "Release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux-showcase", + "displayName": "Showcase for Linux", + "configurePreset": "linux", + "description": "Showcase - Build for Linux", + "targets": ["showcase"], "condition": { "type": "equals", "lhs": "${hostSystemName}", diff --git a/README.md b/README.md index 84475e3..ad73cb1 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ Modern QStyle for desktop Qt6 applications. See [documentation](https://oclero.github.io/qlementine) for more information. +
+ + +
+ --- ### Table of Contents diff --git a/branding/icon/icon.psd b/branding/icon/icon.psd deleted file mode 100644 index 58c095c..0000000 Binary files a/branding/icon/icon.psd and /dev/null differ diff --git a/branding/screenshots/windows-dark.png b/branding/screenshots/windows-dark.png new file mode 100644 index 0000000..32a5320 Binary files /dev/null and b/branding/screenshots/windows-dark.png differ diff --git a/branding/screenshots/windows-light.png b/branding/screenshots/windows-light.png new file mode 100644 index 0000000..9fbba9f Binary files /dev/null and b/branding/screenshots/windows-light.png differ diff --git a/docs/theme.md b/docs/theme.md index 277569d..ac4b3e9 100644 --- a/docs/theme.md +++ b/docs/theme.md @@ -24,25 +24,29 @@ Not all the `Theme` struct members are customizable, because some are used as ca ### Metadata -Metadata must be a JSON object for the key `meta`. It is not mandatory, but might be useful if a GUI to switch themes is implemented someday. +Metadata must be a JSON object for the key `meta`. + +!!! info + + It is mandatory if you want to use `ThemeManager`, because the theme name is used as the identifier that should be unique for every theme. Example: ```json { "meta": { - "author": "Olivier Cléro", - "name": "Light", + "author": "John Doe", + "name": "My Awesome Theme", "version": "1.2.3" } } ``` -| Key | Type | Role | -|:----|:----:|:-----| -| `name` | string | Name of the theme.
Example: `"Light Theme"` | -| `version` | string | Version of the theme.
Example: `"1.0.0"` | -| `author` | string | Author of the theme.
Example: `"John Doe"` | +| Key | Type | Role | +| :-------- | :----: | :--------------------------------------------------- | +| `name` | string | Name of the theme.
Example: `"My Awesome Theme"` | +| `version` | string | Version of the theme.
Example: `"1.2.3"` | +| `author` | string | Author of the theme.
Example: `"John Doe"` | ### Colors @@ -54,72 +58,74 @@ Example: } ``` -| Key | Type | Role | Default | -|:----|:----:|:-----|:--------| -| **`backgroundColorMain1`** | color | Textfields, checkboxes, radiobuttons background. | `#ffffff` | -| `backgroundColorMain2` | color | Window background. | `#f3f3f3` | -| `backgroundColorMain3` | color | Container background, more contrast. | `#e3e3e3` | -| `backgroundColorMain4` | color | Same as above, more contrast. | `#dcdcdc` | -| **`neutralColor`** | color | Neutral interactive elements, such as buttons. | `#e1e1e1` | -| `neutralColorHovered` | color | Same as above, in hovered state. | `#dadada` | -| `neutralColorPressed` | color | Same as above, in pressed state. | `#d2d2d2` | -| `neutralColorDisabled` | color | Same as above, in disabled state. | `#eeeeee` | -| **`focusColor`** | color | Border around the widget that has keyboard focus. | `#40a9ff66` | -| **`primaryColor`** | color | Highlighted elements (default, checked or selected). | `#1890ff` | -| `primaryColorHovered` | color | Same as above, in hovered state. | `#2c9dff` | -| `primaryColorPressed` | color | Same as above, in pressed state. | `#40a9ff` | -| `primaryColorDisabled` | color | Same as above, in disabled state. | `#d1e9ff` | -| **`primaryColorForeground`** | color | Text written over highlighted elements. | `#ffffff` | -| `primaryColorForegroundHovered` | color | Same as above, in hovered state. | `#ffffff` | -| `primaryColorForegroundPressed` | color | Same as above, in pressed state. | `#ffffff` | -| `primaryColorForegroundDisabled` | color | Same as above, in disabled state. | `#ecf6ff` | -| **`primaryAlternativeColor`** | color | A darker/lighter tint for highlighted elements over already highlighted elements. | `#106ef9` | -| `primaryAlternativeColorHovered` | color | Same as above, in hovered state. | `#0f7bfd` | -| `primaryAlternativeColorPressed` | color | Same as above, in pressed state. | `#0f88fd` | -| `primaryAlternativeColorDisabled` | color | Same as above, in disabled state. | `#a9d6ff` | -| **`secondaryColor`** | color | Text. | `#404040` | -| `secondaryColorHovered` | color | Same as above, in hovered state. | `#333333` | -| `secondaryColorPressed` | color | Same as above, in pressed state. | `#262626` | -| `secondaryColorDisabled` | color | Same as above, in disabled state. | `#d4d4d4` | -| **`secondaryColorForeground`** | color | | `#ffffff` | -| `secondaryColorForegroundHovered` | color | Same as above, in hovered state. | `#ffffff` | -| `secondaryColorForegroundPressed` | color | Same as above, in pressed state. | `#ffffff` | -| `secondaryColorForegroundDisabled` | color | Same as above, in disabled state. | `#ededed` | -| **`secondaryAlternativeColor`** | color | Less important text. | `#909090` | -| `secondaryAlternativeColorHovered` | color | Same as above, in hovered state. | `#747474` | -| `secondaryAlternativeColorPressed` | color | Same as above, in pressed state. | `#828282` | -| `secondaryAlternativeColorDisabled` | color | Same as above, in disabled state. | `#c3c3c3` | -| **`statusColorSuccess`** | color | Feedback for success/validity. | `#2bb5a0` | -| `statusColorSuccessHovered` | color | Same as above, in hovered state. | `#3cbfab` | -| `statusColorSuccessPressed` | color | Same as above, in pressed state. | `#4ecdb9` | -| `statusColorSuccessDisabled` | color | Same as above, in disabled state. | `#d5f0ec` | -| **`statusColorInfo`** | color | Feedback for information. | `#1ba8d5` | -| `statusColorInfoHovered` | color | Same as above, in hovered state. | `#1eb5e5` | -| `statusColorInfoPressed` | color | Same as above, in pressed state. | `#29c0f0` | -| `statusColorInfoDisabled` | color | Same as above, in disabled state. | `#c7eaf5` | -| **`statusColorWarning`** | color | Feedback for warning. | `#fbc064` | -| `statusColorWarningHovered` | color | Same as above, in hovered state. | `#ffcf6c` | -| `statusColorWarningPressed` | color | Same as above, in pressed state. | `#ffd880` | -| `statusColorWarningDisabled` | color | Same as above, in disabled state. | `#feefd8` | -| **`statusColorError`** | color | Feedback for error. | `#e96b72` | -| `statusColorErrorHovered` | color | Same as above, in hovered state. | `#f47c83` | -| `statusColorErrorPressed` | color | Same as above, in pressed state. | `#ff9197` | -| `statusColorErrorDisabled` | color | Same as above, in disabled state. | `#f9dadc` | -| **`statusColorForeground`** | color | Text over status colors. | `#ffffff` | -| `statusColorForegroundHovered` | color | Same as above, in hovered state. | `#ffffff` | -| `statusColorForegroundPressed` | color | Same as above, in pressed state. | `#ffffff` | -| `statusColorForegroundDisabled` | color | Same as above, in disabled state. | `#ffffff99` | -| **`shadowColor1`** | color | Shadow for elevated elements. | `#00000020` | -| `shadowColor2` | color | Same as above, more contrast. | `#00000040` | -| `shadowColor3` | color | Same as above, more contrast. | `#00000060` | -| **`borderColor`** | color | Borders of textfields, checkboxes, radiobuttons, switches and other elements. | `#d3d3d3` | -| `borderColorHovered` | color | Same as above, in hovered state. | `#b3b3b3` | -| `borderColorPressed` | color | Same as above, in pressed state. | `#a3a3a3` | -| `borderColorDisabled` | color | Same as above, in disabled state. | `#b3b3b3` | -| **`semiTransparentColor1`** | color | To be used over another color to lighten/darken it. | `#0000000a` | -| `semiTransparentColor2` | color | Same as above but more contrast. | `#00000019` | -| `semiTransparentColor3` | color | Same as above but more contrast. | `#00000021` | -| `semiTransparentColor4` | color | Same as above but more contrast. | `#00000028` | +| Key | Type | Role | +| :---------------------------------- | :---: | :-------------------------------------------------------------------------------- | +| **`backgroundColorMain1`** | color | Textfields, checkboxes, radiobuttons background. | +| `backgroundColorMain2` | color | Window background. | +| `backgroundColorMain3` | color | Container background, more contrast. | +| `backgroundColorMain4` | color | Same as above, more contrast. | +| **`neutralColor`** | color | Neutral interactive elements, such as buttons. | +| `neutralColorHovered` | color | Same as above, in hovered state. | +| `neutralColorPressed` | color | Same as above, in pressed state. | +| `neutralColorDisabled` | color | Same as above, in disabled state. | +| **`focusColor`** | color | Border around the widget that has keyboard focus with `QFocusFrame`. | +| **`backgroundColorWorkspace`** | color | Window workspace backround. | +| **`backgroundColorTabBar`** | color | `QTabBar` backround. | +| **`primaryColor`** | color | Highlighted elements (default, checked or selected). | +| `primaryColorHovered` | color | Same as above, in hovered state. | +| `primaryColorPressed` | color | Same as above, in pressed state. | +| `primaryColorDisabled` | color | Same as above, in disabled state. | +| **`primaryColorForeground`** | color | Text written over highlighted elements. | +| `primaryColorForegroundHovered` | color | Same as above, in hovered state. | +| `primaryColorForegroundPressed` | color | Same as above, in pressed state. | +| `primaryColorForegroundDisabled` | color | Same as above, in disabled state. | +| **`primaryAlternativeColor`** | color | A darker/lighter tint for highlighted elements over already highlighted elements. | +| `primaryAlternativeColorHovered` | color | Same as above, in hovered state. | +| `primaryAlternativeColorPressed` | color | Same as above, in pressed state. | +| `primaryAlternativeColorDisabled` | color | Same as above, in disabled state. | +| **`secondaryColor`** | color | Text. | +| `secondaryColorHovered` | color | Same as above, in hovered state. | +| `secondaryColorPressed` | color | Same as above, in pressed state. | +| `secondaryColorDisabled` | color | Same as above, in disabled state. | +| **`secondaryColorForeground`** | color | Text written over elements that already have text color. | +| `secondaryColorForegroundHovered` | color | Same as above, in hovered state. | +| `secondaryColorForegroundPressed` | color | Same as above, in pressed state. | +| `secondaryColorForegroundDisabled` | color | Same as above, in disabled state. | +| **`secondaryAlternativeColor`** | color | Less important text. | +| `secondaryAlternativeColorHovered` | color | Same as above, in hovered state. | +| `secondaryAlternativeColorPressed` | color | Same as above, in pressed state. | +| `secondaryAlternativeColorDisabled` | color | Same as above, in disabled state. | +| **`statusColorSuccess`** | color | Feedback for success/validity. | +| `statusColorSuccessHovered` | color | Same as above, in hovered state. | +| `statusColorSuccessPressed` | color | Same as above, in pressed state. | +| `statusColorSuccessDisabled` | color | Same as above, in disabled state. | +| **`statusColorInfo`** | color | Feedback for information. | +| `statusColorInfoHovered` | color | Same as above, in hovered state. | +| `statusColorInfoPressed` | color | Same as above, in pressed state. | +| `statusColorInfoDisabled` | color | Same as above, in disabled state. | +| **`statusColorWarning`** | color | Feedback for warning. | +| `statusColorWarningHovered` | color | Same as above, in hovered state. | +| `statusColorWarningPressed` | color | Same as above, in pressed state. | +| `statusColorWarningDisabled` | color | Same as above, in disabled state. | +| **`statusColorError`** | color | Feedback for error. | +| `statusColorErrorHovered` | color | Same as above, in hovered state. | +| `statusColorErrorPressed` | color | Same as above, in pressed state. | +| `statusColorErrorDisabled` | color | Same as above, in disabled state. | +| **`statusColorForeground`** | color | Text over status colors. | +| `statusColorForegroundHovered` | color | Same as above, in hovered state. | +| `statusColorForegroundPressed` | color | Same as above, in pressed state. | +| `statusColorForegroundDisabled` | color | Same as above, in disabled state. | +| **`shadowColor1`** | color | Shadow for elevated elements. | +| `shadowColor2` | color | Same as above, more contrast. | +| `shadowColor3` | color | Same as above, more contrast. | +| **`borderColor`** | color | Borders of textfields, checkboxes, radiobuttons, switches and other elements. | +| `borderColorHovered` | color | Same as above, in hovered state. | +| `borderColorPressed` | color | Same as above, in pressed state. | +| `borderColorDisabled` | color | Same as above, in disabled state. | +| **`semiTransparentColor1`** | color | To be used over another color to lighten/darken it. | +| `semiTransparentColor2` | color | Same as above but more contrast. | +| `semiTransparentColor3` | color | Same as above but more contrast. | +| `semiTransparentColor4` | color | Same as above but more contrast. | ### Numeric Values @@ -131,47 +137,47 @@ Example: } ``` -| Key | Type | Role | Default | -|:----|:----:|:-----|:-------:| -| `fontSize` | int | Font size for normal text. | `12` | -| `fontSizeMonospace` | int | Font size for monospace text. | `13` | -| `fontSizeH1` | int | Font size for level 1 headers. | `34` | -| `fontSizeH2` | int | Font size for level 2 headers. | `26` | -| `fontSizeH3` | int | Font size for level 3 headers. | `22` | -| `fontSizeH4` | int | Font size for level 4 headers. | `18` | -| `fontSizeH5` | int | Font size for level 4 headers. | `14` | -| `fontSizeS1` | int | Font size for level 1 captions. | `10` | -| `spacing` | int | Spacing between elements. Multiples of this value will be used across the various widgets.| `8` | -| `iconExtent` | int | Size for icons. Multiple of this value will be used across the various widgets, according to their size. | `16` | -| `animationDuration` | int | Duration (in milliseconds) for a UI animation, such as a color change. | `192` | -| `focusAnimationDuration` | int | Duration (in milliseconds) for the focus border animation.
Note: can be longer to allow the user to see the focus . | `384` | -| `sliderAnimationDuration` | int | Duration (in milliseconds) for the slider animation when its value changes.
Note: must be quick to feel responsive. | `96` | -| `borderRadius` | double | Corner radius for most widgets. | `6.0` | -| `checkBoxBorderRadius` | double | Corner radius for checkboxes.
Note: smaller than `borderRadius` because checkboxes are smaller. | `4.0` | -| `menuItemBorderRadius` | double | Corner radius for menu items.
Note: Even smaller than `borderRadius` because menu already have a corner radius and padding. | `4.0` | -| `menuBarItemBorderRadius` | double | Corner radius for menu bar items. | `2.0` | -| `borderWidth` | double | Border thickness for widgets that have borders. | `1` | -| `focusBorderWidth` | int | Border thickness for the focus indicator. | `2` | -| `controlHeightLarge` | int | Height for most basics `QWidget`s, such as `QPushButton` or `QCheckBox`. | `28` | -| `controlHeightMedium` | int | Height for a smaller `QWidget`, such as `QSlider`. | `24` | -| `controlHeightSmall` | int | Height for an even smaller `QWidget`, such as the scroll buttons of a `QMenu`. | `16` | -| `controlDefaultWidth` | int | Width used as the default width in `sizeHint()` method for widgets that are extensible, such as `QSlider` or `QProgressBar`. | `96` | -| `dialMarkLength` | int | For a `QDial`, the length of the knob needle that indicates its value. | `8` | -| `dialMarkThickness` | int | For a `QDial`, the thickness of a tick, if visible. | `2` | -| `dialTickLength` | int | For a `QDial`, the length of a tick, if visible. | `4` | -| `dialTickSpacing` | int | For a `QDial`, the spacing between knob and ticks, if visible. | `4` | -| `dialGrooveThickness` | int | For a `QDial`, the thickness of the knob highlighted zone. | `4` | -| `sliderTickSize` | int | For a `QSlider`, the length of a tick, if visible. | `3` | -| `sliderTickSpacing` | int | For a `QSlider`, the spacing between slider and ticks, if visible. | `2` | -| `sliderTickThickness` | int | For a `QSlider`, the thickness of a tick, if visible.| `1` | -| `sliderGrooveHeight` | int | For a `QSlider`, the thickness of the groove (i.e. the highlited zone). | `4` | -| `progressBarGrooveHeight` | int | For a `QProgressBar`, the thickness of the groove (i.e. the highlited zone). | `6` | -| `scrollBarThicknessFull` | int | For a `QScrollBar`, the thickness when the mouse is over. | `12` | -| `scrollBarThicknessSmall` | int | For a `QScrollBar`, the thickness when the mouse is not hover. | `6` | -| `scrollBarMargin` | int | For a `QScrollBar`, the margin between the scrollbar and its parent. | `0` | -| `tabBarPaddingTop` | int | For a `QTabBar`, the space between the top of the bar and the top of the tabs. | `4` | -| `tabBarTabMaxWidth` | int | For a `QTabBar`, the maximum width a tab can have.
Note: any value below or equal to `0` will be ignored and treated as if there is no maximum width. | `0` | -| `tabBarTabMinWidth` | int | For a `QTabBar`, the minimum width a tab can have.
Note: any value below or equal to `0` will be ignored and treated as if there is no minimum width. | `0` | +| Key | Type | Role | +| :------------------------ | :----: | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fontSize` | int | Font size for normal text (`QLabel`, `QPushButton`, `QCheckBox`, etc.). | +| `fontSizeMonospace` | int | Font size for monospace text. | +| `fontSizeH1` | int | Font size for level 1 headers. | +| `fontSizeH2` | int | Font size for level 2 headers. | +| `fontSizeH3` | int | Font size for level 3 headers. | +| `fontSizeH4` | int | Font size for level 4 headers. | +| `fontSizeH5` | int | Font size for level 4 headers. | +| `fontSizeS1` | int | Font size for level 1 captions. | +| `spacing` | int | Spacing between elements. Multiples of this value will be used across the various widgets, including default `QLayout` spacings and margins. | +| `iconExtent` | int | Size for icons. Multiple of this value will be used across the various widgets, according to their size for `QIcon`. | +| `animationDuration` | int | Duration (in milliseconds) for a UI animation, such as a color change. | +| `focusAnimationDuration` | int | Duration (in milliseconds) for the focus border animation.
Note: can be longer to allow the user to see the focus . | +| `sliderAnimationDuration` | int | Duration (in milliseconds) for the slider animation when its value changes.
Note: must be quick to feel responsive. | +| `borderRadius` | double | Corner radius for most widgets. | +| `checkBoxBorderRadius` | double | Corner radius for checkboxes.
Note: smaller than `borderRadius` because checkboxes are smaller. | +| `menuItemBorderRadius` | double | Corner radius for menu items.
Note: Even smaller than `borderRadius` because menu already have a corner radius and padding. | +| `menuBarItemBorderRadius` | double | Corner radius for menu bar items. | +| `borderWidth` | double | Border thickness for widgets that have borders. | +| `focusBorderWidth` | int | Border thickness for the focus indicator. | +| `controlHeightLarge` | int | Height for most basics `QWidget`s, such as `QPushButton` or `QCheckBox`. | +| `controlHeightMedium` | int | Height for a smaller `QWidget`, such as `QSlider`. | +| `controlHeightSmall` | int | Height for an even smaller `QWidget`, such as the scroll buttons of a `QMenu`. | +| `controlDefaultWidth` | int | Width used as the default width in `sizeHint()` method for widgets that are extensible, such as `QSlider` or `QProgressBar`. | +| `dialMarkLength` | int | For a `QDial`, the length of the knob needle that indicates its value. | +| `dialMarkThickness` | int | For a `QDial`, the thickness of a tick, if visible. | +| `dialTickLength` | int | For a `QDial`, the length of a tick, if visible. | +| `dialTickSpacing` | int | For a `QDial`, the spacing between knob and ticks, if visible. | +| `dialGrooveThickness` | int | For a `QDial`, the thickness of the knob highlighted zone. | +| `sliderTickSize` | int | For a `QSlider`, the length of a tick, if visible. | +| `sliderTickSpacing` | int | For a `QSlider`, the spacing between slider and ticks, if visible. | +| `sliderTickThickness` | int | For a `QSlider`, the thickness of a tick, if visible. | +| `sliderGrooveHeight` | int | For a `QSlider`, the thickness of the groove (i.e. the highlited zone). | +| `progressBarGrooveHeight` | int | For a `QProgressBar`, the thickness of the groove (i.e. the highlited zone). | +| `scrollBarThicknessFull` | int | For a `QScrollBar`, the thickness when the mouse is over. | +| `scrollBarThicknessSmall` | int | For a `QScrollBar`, the thickness when the mouse is not hover. | +| `scrollBarMargin` | int | For a `QScrollBar`, the margin between the scrollbar and its parent. | +| `tabBarPaddingTop` | int | For a `QTabBar`, the space between the top of the bar and the top of the tabs. | +| `tabBarTabMaxWidth` | int | For a `QTabBar`, the maximum width a tab can have.
Note: any value below or equal to `0` will be ignored and treated as if there is no maximum width. | +| `tabBarTabMinWidth` | int | For a `QTabBar`, the minimum width a tab can have.
Note: any value below or equal to `0` will be ignored and treated as if there is no minimum width. | ## Full example @@ -182,55 +188,58 @@ Here is a full Qlementine theme in all its glory. Please note that every value i "meta": { "author": "Olivier Cléro", "name": "Light", - "version": "1.4.0" + "version": "1.5.0" }, "backgroundColorMain1": "#ffffff", "backgroundColorMain2": "#f3f3f3", "backgroundColorMain3": "#e3e3e3", - "backgroundColorMain4": "#dcdcdc", + "backgroundColorMain4": "#dfdfdf", + + "backgroundColorWorkspace": "#b7b7b7", + "backgroundColorTabBar": "#dfdfdf", "borderColor": "#d3d3d3", + "borderColorDisabled": "#e9e9e9", "borderColorHovered": "#b3b3b3", "borderColorPressed": "#a3a3a3", - "borderColorDisabled": "#e9e9e9", "focusColor": "#40a9ff66", - "neutralColor": "#e1e1e1", - "neutralColorHovered": "#d9d9d9", - "neutralColorPressed": "#d2d2d2", + "neutralColor": "#d1d1d1", + "neutralColorHovered": "#d3d3d3", + "neutralColorPressed": "#d5d5d5", "neutralColorDisabled": "#eeeeee", - "primaryAlternativeColor": "#106ef9", - "primaryAlternativeColorHovered": "#107bfd", - "primaryAlternativeColorPressed": "#108bfd", - "primaryAlternativeColorDisabled": "#a9d6ff", - "primaryColor": "#1890ff", "primaryColorHovered": "#2c9dff", - "primaryColorDisabled": "#d1e9ff", "primaryColorPressed": "#40a9ff", + "primaryColorDisabled": "#d1e9ff", + + "primaryAlternativeColor": "#106ef9", + "primaryAlternativeColorDisabled": "#a9d6ff", + "primaryAlternativeColorHovered": "#107bfd", + "primaryAlternativeColorPressed": "#108bfd", "primaryColorForeground": "#ffffff", + "primaryColorForegroundDisabled": "#ecf6ff", "primaryColorForegroundHovered": "#ffffff", "primaryColorForegroundPressed": "#ffffff", - "primaryColorForegroundDisabled": "#ecf6ff", - - "secondaryAlternativeColor": "#909090", - "secondaryAlternativeColorHovered": "#747474", - "secondaryAlternativeColorPressed": "#828282", - "secondaryAlternativeColorDisabled": "#c3c3c3", "secondaryColor": "#404040", "secondaryColorHovered": "#333333", "secondaryColorPressed": "#262626", "secondaryColorDisabled": "#d4d4d4", + "secondaryAlternativeColor": "#909090", + "secondaryAlternativeColorDisabled": "#c3c3c3", + "secondaryAlternativeColorHovered": "#747474", + "secondaryAlternativeColorPressed": "#828282", + "secondaryColorForeground": "#ffffff", + "secondaryColorForegroundDisabled": "#ededed", "secondaryColorForegroundHovered": "#ffffff", "secondaryColorForegroundPressed": "#ffffff", - "secondaryColorForegroundDisabled": "#ededed", "semiTransparentColor1": "#0000000a", "semiTransparentColor2": "#00000019", @@ -241,16 +250,16 @@ Here is a full Qlementine theme in all its glory. Please note that every value i "shadowColor2": "#00000040", "shadowColor3": "#00000060", - "statusColorForeground": "#ffffff", - "statusColorForegroundDisabled": "#ffffff99", - "statusColorForegroundHovered": "#ffffff", - "statusColorForegroundPressed": "#ffffff", - "statusColorError": "#e96b72", "statusColorErrorHovered": "#f47c83", "statusColorErrorPressed": "#ff9197", "statusColorErrorDisabled": "#f9dadc", + "statusColorForeground": "#ffffff", + "statusColorForegroundHovered": "#ffffff", + "statusColorForegroundPressed": "#ffffff", + "statusColorForegroundDisabled": "#ffffff99", + "statusColorInfo": "#1ba8d5", "statusColorInfoHovered": "#1eb5e5", "statusColorInfoPressed": "#29c0f0", diff --git a/docs/usage.md b/docs/usage.md index 07385f4..bdb26ed 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -26,6 +26,26 @@ Define the `QStyle` on your `QApplication`. QApplication app(argc, argv); auto* style = new oclero::qlementine::QlementineStyle(&app); -style->setThemeJsonPath(":/light.json"); QApplication::setStyle(style); ``` + +## Themes + +You may want to use your own JSON theme. + +```c++ +style->setThemeJsonPath(":/path/to/your/theme.json"); +``` + +Additionnally, you can also use `ThemeManager` to handle that for you. + +```c++ +// Link a ThemeManager to a QlementineStyle. +auto* themeManager = new oclero::qlementine::ThemeManager(style); + +// Load the directory where you store your own JSON themes. +themeManager->loadDirectory(":/themes"); + +// Define theme on QStyle. +themeManager->setCurrentTheme("Light"); +``` diff --git a/docs/widgets.md b/docs/widgets.md index ecea3ce..9a63127 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -6,7 +6,7 @@ More information about them coming soon. You can check them in the Sandbox appli ## AbstractItemListWidget -Base class for [`NavigationBar`](#navigation-bar) and [`SegmentedControl`](#segmented-control). +Base class for [`NavigationBar`](#navigationbar) and [`SegmentedControl`](#segmentedcontrol). ## Action @@ -80,13 +80,13 @@ Improves `QFocusFrame` by adding a corner radius property. A widget that allows to switch between a range of elements, such as seen on iOS or macOS. -![SegmentedControl](assets/images/widgets/segmentedcontrol) +![SegmentedControl](assets/images/widgets/segmentedcontrol.png) ## StatusBadgeWidget Widget to display a status icon: info, warning, error, success. Available in two standard sizes. -![StatusBadgeWidget](assets/images/widgets/badges) +![StatusBadgeWidget](assets/images/widgets/badges.png) ## Switch diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 496ba05..05ee9a6 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -20,7 +20,10 @@ set(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/BadgeUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/ColorUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FontUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/IconUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/ImageUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/LayoutUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/MenuUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/PrimitiveUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/RadiusesF.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StateUtils.cpp @@ -69,7 +72,10 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/BadgeUtils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/ColorUtils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/FontUtils.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/IconUtils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/ImageUtils.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/LayoutUtils.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/MenuUtils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/PrimitiveUtils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/RadiusesF.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/oclero/qlementine/utils/StateUtils.hpp @@ -120,34 +126,37 @@ target_include_directories(${LIB_TARGET_NAME} $ ) -target_link_libraries(${LIB_TARGET_NAME} PUBLIC - Qt6::Core - Qt6::Widgets - Qt6::Svg +target_link_libraries(${LIB_TARGET_NAME} + PUBLIC + Qt::Core + Qt::Widgets + Qt::Svg ) set_target_properties(${LIB_TARGET_NAME} PROPERTIES - OUTPUT_NAME ${LIB_TARGET_NAME} - PROJECT_LABEL ${LIB_TARGET_NAME} - FOLDER lib - SOVERSION ${PROJECT_VERSION_MAJOR} - VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} - DEBUG_POSTFIX _debug + OUTPUT_NAME ${LIB_TARGET_NAME} + PROJECT_LABEL ${LIB_TARGET_NAME} + FOLDER lib + SOVERSION ${PROJECT_VERSION_MAJOR} + VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + DEBUG_POSTFIX _debug CMAKE_AUTORCC ON CMAKE_AUTOMOC ON CMAKE_AUTOUIC ON ) -target_compile_options(${LIB_TARGET_NAME} PRIVATE - $<$:/MP /WX /W4> - $<$>:-Wall -Wextra -Werror> +target_compile_options(${LIB_TARGET_NAME} + PRIVATE + $<$:/MP /WX /W4> + $<$>:-Wall -Wextra -Werror> ) # Create source groups. -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES - ${HEADERS} - ${SOURCES} +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} + FILES + ${HEADERS} + ${SOURCES} ) # Select correct startup project in Visual Studio. diff --git a/lib/include/oclero/qlementine/style/QlementineStyle.hpp b/lib/include/oclero/qlementine/style/QlementineStyle.hpp index 46bf425..45d1256 100644 --- a/lib/include/oclero/qlementine/style/QlementineStyle.hpp +++ b/lib/include/oclero/qlementine/style/QlementineStyle.hpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -20,8 +21,6 @@ class QlementineStyle : public QCommonStyle { Q_OBJECT Q_PROPERTY(bool animationsEnabled READ animationsEnabled WRITE setAnimationsEnabled NOTIFY animationsEnabledChanged) - Q_PROPERTY(bool useMenuForComboBoxPopup READ useMenuForComboBoxPopup WRITE setUseMenuForComboBoxPopup NOTIFY - useMenuForComboBoxPopupChanged) public: enum class StandardPixmapExt { @@ -60,10 +59,6 @@ class QlementineStyle : public QCommonStyle { void setAnimationsEnabled(bool enabled); Q_SIGNAL void animationsEnabledChanged(); - bool useMenuForComboBoxPopup() const; - void setUseMenuForComboBoxPopup(bool useMenu); - Q_SIGNAL void useMenuForComboBoxPopupChanged(); - void triggerCompleteRepaint(); void setAutoIconColor(AutoIconColor autoIconColor); @@ -74,7 +69,15 @@ class QlementineStyle : public QCommonStyle { QPixmap getColorizedPixmap( const QPixmap& input, AutoIconColor autoIconColor, const QColor& fgcolor, const QColor& textColor) const; - static QIcon makeIcon(const QString& svgPath); + + QIcon makeThemedIcon( + const QString& svgPath, const QSize& size = QSize(16, 16), ColorRole role = ColorRole::Secondary) const; + + QIcon makeThemedIconFromName( + const QString& name, const QSize& size = QSize(16, 16), ColorRole role = ColorRole::Secondary) const; + + // Allows to customize quickly the way QlementineStyle gets its icons. SVG paths preferred. + void setIconPathGetter(const std::function& func); public: // QStyle overrides. void drawPrimitive( @@ -204,7 +207,7 @@ class QlementineStyle : public QCommonStyle { virtual QColor const& menuBarItemBackgroundColor(MouseState const mouse, SelectionState const selected) const; virtual QColor const& menuBarItemForegroundColor(MouseState const mouse, SelectionState const selected) const; - virtual QColor const& tabBarBackgroundColor() const; + virtual QColor const& tabBarBackgroundColor(MouseState const mouse) const; virtual QColor const& tabBarShadowColor() const; virtual QColor const& tabBarBottomShadowColor() const; virtual QColor const& tabBackgroundColor(MouseState const mouse, SelectionState const selected) const; @@ -237,6 +240,8 @@ class QlementineStyle : public QCommonStyle { virtual QColor const& labelForegroundColor(MouseState const mouse, const QWidget* w = nullptr) const; virtual QColor const& labelCaptionForegroundColor(MouseState const mouse) const; + virtual QColor const& iconForegroundColor(MouseState const mouse, ColorRole const role) const; + virtual QColor const& toolBarBackgroundColor() const; virtual QColor const& toolBarBorderColor() const; virtual QColor const& toolBarSeparatorColor() const; @@ -251,7 +256,7 @@ class QlementineStyle : public QCommonStyle { virtual QColor const& groupBoxTitleColor(MouseState const mouse, const QWidget* w = nullptr) const; virtual QColor const& groupBoxBorderColor(MouseState const mouse) const; - virtual QColor const& groupBoxBackgroundColor(MouseState const mouse) const; + virtual QColor groupBoxBackgroundColor(MouseState const mouse) const; virtual QColor const& statusColor(Status const status, MouseState const mouse) const; virtual QColor focusBorderColor(Status status) const; @@ -281,7 +286,11 @@ class QlementineStyle : public QCommonStyle { virtual QColor const& statusBarBorderColor() const; virtual QColor const& statusBarSeparatorColor() const; + virtual QColor const& splitterColor(MouseState const mouse) const; + private: std::unique_ptr _impl; }; + +QlementineStyle* appStyle(); } // namespace oclero::qlementine diff --git a/lib/include/oclero/qlementine/style/Theme.hpp b/lib/include/oclero/qlementine/style/Theme.hpp index 069a65c..32c3a6a 100644 --- a/lib/include/oclero/qlementine/style/Theme.hpp +++ b/lib/include/oclero/qlementine/style/Theme.hpp @@ -3,6 +3,8 @@ #pragma once +#include + #include #include @@ -32,8 +34,9 @@ struct ThemeMeta { class Theme { public: // Ctor. Theme(); - Theme(QString const& jsonPath); - explicit Theme(QJsonDocument const& jsonDoc); + + static std::optional fromJsonPath(const QString& jsonPath); + static std::optional fromJsonDoc(const QJsonDocument& jsonDoc); Theme(Theme const& other) = default; Theme(Theme&& other) = default; @@ -48,89 +51,92 @@ class Theme { public: // Values. ThemeMeta meta; - QColor backgroundColorMain1{ 0xFFFFFF }; - QColor backgroundColorMain2{ 0xF3F3F3 }; - QColor backgroundColorMain3{ 0xE3E3E3 }; - QColor backgroundColorMain4{ 0xDCDCDC }; - QColor backgroundColorMainTransparent{ QRgba64::fromArgb32(0x00FAFAFA) }; + QColor backgroundColorMain1{ 0xffffff }; + QColor backgroundColorMain2{ 0xf3f3f3 }; + QColor backgroundColorMain3{ 0xe3e3e3 }; + QColor backgroundColorMain4{ 0xdcdcdc }; + QColor backgroundColorMainTransparent{ QRgba64::fromArgb32(0x00fafafa) }; + + QColor backgroundColorWorkspace{ 0xb7b7b7 }; + QColor backgroundColorTabBar{ 0xb7b7b7 }; - QColor neutralColor{ 0xE1E1E1 }; - QColor neutralColorHovered{ 0xDADADA }; - QColor neutralColorPressed{ 0xD2D2D2 }; - QColor neutralColorDisabled{ 0xEEEEEE }; + QColor neutralColor{ 0xe1e1e1 }; + QColor neutralColorHovered{ 0xdadada }; + QColor neutralColorPressed{ 0xd2d2d2 }; + QColor neutralColorDisabled{ 0xeeeeee }; QColor neutralColorTransparent{ QRgba64::fromArgb32(0x00E1E1E1) }; QColor focusColor{ QRgba64::fromArgb32(0x6640a9ff) }; - QColor primaryColor{ 0x1890FF }; - QColor primaryColorHovered{ 0x2C9DFF }; - QColor primaryColorPressed{ 0x40A9FF }; - QColor primaryColorDisabled{ 0xD1E9FF }; + QColor primaryColor{ 0x1890ff }; + QColor primaryColorHovered{ 0x2c9dff }; + QColor primaryColorPressed{ 0x40a9ff }; + QColor primaryColorDisabled{ 0xd1e9ff }; QColor primaryColorTransparent{ QRgba64::fromArgb32(0x001890FF) }; - QColor primaryColorForeground{ 0xFFFFFF }; - QColor primaryColorForegroundHovered{ 0xFFFFFF }; - QColor primaryColorForegroundPressed{ 0xFFFFFF }; - QColor primaryColorForegroundDisabled{ 0xECF6FF }; - QColor primaryColorForegroundTransparent{ QRgba64::fromArgb32(0x00FFFFFF) }; + QColor primaryColorForeground{ 0xffffff }; + QColor primaryColorForegroundHovered{ 0xffffff }; + QColor primaryColorForegroundPressed{ 0xffffff }; + QColor primaryColorForegroundDisabled{ 0xecf6ff }; + QColor primaryColorForegroundTransparent{ QRgba64::fromArgb32(0x00ffffff) }; - QColor primaryAlternativeColor{ 0x106EF9 }; - QColor primaryAlternativeColorHovered{ 0x0F7BFD }; - QColor primaryAlternativeColorPressed{ 0x0F8BFD }; + QColor primaryAlternativeColor{ 0x106ef9 }; + QColor primaryAlternativeColorHovered{ 0x107bfd }; + QColor primaryAlternativeColorPressed{ 0x108bfd }; QColor primaryAlternativeColorDisabled{ 0xa9d6ff }; QColor primaryAlternativeColorTransparent{ QRgba64::fromArgb32(0x001875ff) }; QColor secondaryColor{ 0x404040 }; QColor secondaryColorHovered{ 0x333333 }; QColor secondaryColorPressed{ 0x262626 }; - QColor secondaryColorDisabled{ 0xD4D4D4 }; + QColor secondaryColorDisabled{ 0xd4d4d4 }; QColor secondaryColorTransparent{ QRgba64::fromArgb32(0x00404040) }; - QColor secondaryColorForeground{ 0xFFFFFF }; - QColor secondaryColorForegroundHovered{ 0xFFFFFF }; - QColor secondaryColorForegroundPressed{ 0xFFFFFF }; - QColor secondaryColorForegroundDisabled{ 0xEDEDED }; - QColor secondaryColorForegroundTransparent{ QRgba64::fromArgb32(0x00FFFFFF) }; + QColor secondaryColorForeground{ 0xffffff }; + QColor secondaryColorForegroundHovered{ 0xffffff }; + QColor secondaryColorForegroundPressed{ 0xffffff }; + QColor secondaryColorForegroundDisabled{ 0xededed }; + QColor secondaryColorForegroundTransparent{ QRgba64::fromArgb32(0x00ffffff) }; QColor secondaryAlternativeColor{ 0x909090 }; QColor secondaryAlternativeColorHovered{ 0x747474 }; QColor secondaryAlternativeColorPressed{ 0x828282 }; - QColor secondaryAlternativeColorDisabled{ 0xC3C3C3 }; + QColor secondaryAlternativeColorDisabled{ 0xc3c3c3 }; QColor secondaryAlternativeColorTransparent{ QRgba64::fromArgb32(0x00909090) }; - QColor statusColorSuccess{ 0x2BB5A0 }; - QColor statusColorSuccessHovered{ 0x3CBFAB }; - QColor statusColorSuccessPressed{ 0x4ECDB9 }; - QColor statusColorSuccessDisabled{ 0xD5F0EC }; - QColor statusColorInfo{ 0x1BA8D5 }; - QColor statusColorInfoHovered{ 0x1EB5E5 }; + QColor statusColorSuccess{ 0x2bb5a0 }; + QColor statusColorSuccessHovered{ 0x3cbfab }; + QColor statusColorSuccessPressed{ 0x4ecdb9 }; + QColor statusColorSuccessDisabled{ 0xd5f0ec }; + QColor statusColorInfo{ 0x1ba8d5 }; + QColor statusColorInfoHovered{ 0x1eb5e5 }; QColor statusColorInfoPressed{ 0x29c0f0 }; - QColor statusColorInfoDisabled{ 0xC7EAF5 }; + QColor statusColorInfoDisabled{ 0xc7eaf5 }; QColor statusColorWarning{ 0xfbc064 }; - QColor statusColorWarningHovered{ 0xFFCF6C }; - QColor statusColorWarningPressed{ 0xFFD880 }; - QColor statusColorWarningDisabled{ 0xFEEFD8 }; - QColor statusColorError{ 0xE96B72 }; - QColor statusColorErrorHovered{ 0xF47C83 }; - QColor statusColorErrorPressed{ 0xFF9197 }; - QColor statusColorErrorDisabled{ 0xF9DADC }; - QColor statusColorForeground{ 0xFFFFFF }; - QColor statusColorForegroundHovered{ 0xFFFFFF }; - QColor statusColorForegroundPressed{ 0xFFFFFF }; - QColor statusColorForegroundDisabled{ QRgba64::fromArgb32(0x99FFFFFF) }; + QColor statusColorWarningHovered{ 0xffcf6c }; + QColor statusColorWarningPressed{ 0xffd880 }; + QColor statusColorWarningDisabled{ 0xfeefd8 }; + QColor statusColorError{ 0xe96b72 }; + QColor statusColorErrorHovered{ 0xf47c83 }; + QColor statusColorErrorPressed{ 0xff9197 }; + QColor statusColorErrorDisabled{ 0xf9dadc }; + QColor statusColorForeground{ 0xffffff }; + QColor statusColorForegroundHovered{ 0xffffff }; + QColor statusColorForegroundPressed{ 0xffffff }; + QColor statusColorForegroundDisabled{ QRgba64::fromArgb32(0x99ffffff) }; QColor shadowColor1{ QRgba64::fromArgb32(0x20000000) }; QColor shadowColor2{ QRgba64::fromArgb32(0x40000000) }; QColor shadowColor3{ QRgba64::fromArgb32(0x60000000) }; QColor shadowColorTransparent{ QRgba64::fromArgb32(0x00000000) }; - QColor borderColor{ 0xD3D3D3 }; - QColor borderColorHovered{ 0xB3B3B3 }; - QColor borderColorPressed{ 0xA3A3A3 }; - QColor borderColorDisabled{ 0xE9E9E9 }; - QColor borderColorTransparent{ QRgba64::fromArgb32(0x00D3D3D3) }; + QColor borderColor{ 0xd3d3d3 }; + QColor borderColorHovered{ 0xb3b3b3 }; + QColor borderColorPressed{ 0xa3a3a3 }; + QColor borderColorDisabled{ 0xe9e9e9 }; + QColor borderColorTransparent{ QRgba64::fromArgb32(0x00d3d3d3) }; - QColor semiTransparentColor1{ QRgba64::fromArgb32(0x0A000000) }; + QColor semiTransparentColor1{ QRgba64::fromArgb32(0x0000000) }; QColor semiTransparentColor2{ QRgba64::fromArgb32(0x19000000) }; QColor semiTransparentColor3{ QRgba64::fromArgb32(0x21000000) }; QColor semiTransparentColor4{ QRgba64::fromArgb32(0x28000000) }; @@ -198,6 +204,6 @@ class Theme { private: void initializeFonts(); void initializePalette(); - void initializeFromJson(QJsonDocument const& jsonDoc); + bool initializeFromJson(QJsonDocument const& jsonDoc); }; } // namespace oclero::qlementine diff --git a/lib/include/oclero/qlementine/style/ThemeManager.hpp b/lib/include/oclero/qlementine/style/ThemeManager.hpp index 5db2d9e..ba66528 100644 --- a/lib/include/oclero/qlementine/style/ThemeManager.hpp +++ b/lib/include/oclero/qlementine/style/ThemeManager.hpp @@ -29,6 +29,8 @@ class ThemeManager : public QObject { const std::vector& themes() const; void addTheme(const Theme& theme); + void loadDirectory(const QString& path); + QString currentTheme() const; void setCurrentTheme(const QString& key); Q_SIGNAL void currentThemeChanged(); @@ -44,8 +46,6 @@ class ThemeManager : public QObject { int currentThemeIndex() const; void setCurrentThemeIndex(int index); - QString getLocalizedThemeName(const QString& baseThemeName) const; - private: void synchronizeThemeOnStyle(); diff --git a/lib/include/oclero/qlementine/utils/ColorUtils.hpp b/lib/include/oclero/qlementine/utils/ColorUtils.hpp index 248dab1..3bc51ca 100644 --- a/lib/include/oclero/qlementine/utils/ColorUtils.hpp +++ b/lib/include/oclero/qlementine/utils/ColorUtils.hpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Olivier Cléro // SPDX-License-Identifier: MIT +#pragma once + #include #include #include diff --git a/lib/include/oclero/qlementine/utils/FontUtils.hpp b/lib/include/oclero/qlementine/utils/FontUtils.hpp index 95d5849..77090ba 100644 --- a/lib/include/oclero/qlementine/utils/FontUtils.hpp +++ b/lib/include/oclero/qlementine/utils/FontUtils.hpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Olivier Cléro // SPDX-License-Identifier: MIT +#pragma once + #include #include diff --git a/lib/include/oclero/qlementine/utils/IconUtils.hpp b/lib/include/oclero/qlementine/utils/IconUtils.hpp new file mode 100644 index 0000000..bdc6e16 --- /dev/null +++ b/lib/include/oclero/qlementine/utils/IconUtils.hpp @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Olivier Cléro +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +namespace oclero::qlementine { +struct IconTheme { + QColor normal; + QColor disabled; + QColor checkedNormal; + QColor checkedDisabled; + + IconTheme(const QColor& normal); + IconTheme(const QColor& normal, const QColor& disabled); + IconTheme(const QColor& normal, const QColor& disabled, const QColor& checkedNormal, QColor checkedDisabled); + + const QColor& color(QIcon::Mode mode, QIcon::State state) const; +}; + +/// Makes an icon from the file located at the path in parameter. Fixes the standard Qt behavior. +[[maybe_unused]] QIcon makeIconFromSvg(const QString& svgPath, const QSize& size); + +/// Makes an icon from the file located at the path in parameter and colorizes the QPixmaps. Fixes the standard Qt behavior. +[[maybe_unused]] QIcon makeIconFromSvg( + const QString& svgPath, const IconTheme& iconTheme, const QSize& size = QSize(16, 16)); +} // namespace oclero::qlementine diff --git a/lib/include/oclero/qlementine/utils/ImageUtils.hpp b/lib/include/oclero/qlementine/utils/ImageUtils.hpp index ad31628..b0e6261 100644 --- a/lib/include/oclero/qlementine/utils/ImageUtils.hpp +++ b/lib/include/oclero/qlementine/utils/ImageUtils.hpp @@ -82,9 +82,6 @@ enum class AutoIconColor { /// Gets the pixmap in the cache, or creates it if not yet there. QPixmap getCachedPixmap(QPixmap const& input, QColor const& color, ColorizeMode mode); -/// Makes an icon from the file located at the path in parameter. Fixes the standard Qt behavior. -QIcon makeIconFromSvg(const QString& svgPath, const QSize& size); - /// Makes a QPixmap from the file located at the path in parameter at the desired size. QPixmap makePixmapFromSvg(const QString& svgPath, const QSize& size); diff --git a/lib/include/oclero/qlementine/utils/LayoutUtils.hpp b/lib/include/oclero/qlementine/utils/LayoutUtils.hpp new file mode 100644 index 0000000..fe4365b --- /dev/null +++ b/lib/include/oclero/qlementine/utils/LayoutUtils.hpp @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Olivier Cléro +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +#include + +class QWidget; + +namespace oclero::qlementine { +/// Retrieves the widget's QStyle margins. +QMargins getLayoutMargins(const QWidget* widget); + +/// Retrieves the widget's QStyle horizontal spacing. +int getLayoutHSpacing(const QWidget* widget); + +/// Retrieves the widget's QStyle vertical spacing. +int getLayoutVSpacing(const QWidget* widget); + +/// Retrieves the widget's QStyle horizontal spacing and margins. +std::tuple getHLayoutProps(const QWidget* widget); + +/// Retrieves the widget's QStyle vertical spacing and margins. +std::tuple getVLayoutProps(const QWidget* widget); + +/// Retrieves the widget's QStyle vertical/horizontal spacings and margins. +std::tuple getFormLayoutProps(const QWidget* widget); + +/// Remove and deletes all the elements in the layout. +void clearLayout(QLayout* layout); +} // namespace oclero::qlementine diff --git a/lib/include/oclero/qlementine/utils/MenuUtils.hpp b/lib/include/oclero/qlementine/utils/MenuUtils.hpp new file mode 100644 index 0000000..95fd51e --- /dev/null +++ b/lib/include/oclero/qlementine/utils/MenuUtils.hpp @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Olivier Cléro +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +class QMenu; +class QAction; + +namespace oclero::qlementine { +QMenu* getTopLevelMenu(QMenu* menu); + +void flashAction(QAction* action, QMenu* menu, const std::function& onAnimationFinished); +} // namespace oclero::qlementine diff --git a/lib/include/oclero/qlementine/utils/PrimitiveUtils.hpp b/lib/include/oclero/qlementine/utils/PrimitiveUtils.hpp index e715a0f..805716d 100644 --- a/lib/include/oclero/qlementine/utils/PrimitiveUtils.hpp +++ b/lib/include/oclero/qlementine/utils/PrimitiveUtils.hpp @@ -14,7 +14,7 @@ #include namespace oclero::qlementine { -static constexpr auto QLEMENTINE_PI = 3.14159265358979323846; +[[maybe_unused]] static constexpr auto QLEMENTINE_PI = 3.14159265358979323846; /// Gets the device pixel ratio for the QWidget. double getPixelRatio(QWidget const* w); diff --git a/lib/include/oclero/qlementine/utils/WidgetUtils.hpp b/lib/include/oclero/qlementine/utils/WidgetUtils.hpp index e8d33cc..ae579b9 100644 --- a/lib/include/oclero/qlementine/utils/WidgetUtils.hpp +++ b/lib/include/oclero/qlementine/utils/WidgetUtils.hpp @@ -13,14 +13,10 @@ QWidget* makeHorizontalLine(QWidget* parentWidget, int maxWidth = -1); void centerWidget(QWidget* widget, QWidget* host = nullptr); -QMargins getDefaultMargins(const QStyle* style); - qreal getDpi(const QWidget* widget); QWindow* getWindow(const QWidget* widget); -void clearLayout(QLayout* layout); - template T* findFirstParentOfType(QWidget* child) { auto* parent = child; diff --git a/lib/include/oclero/qlementine/widgets/Expander.hpp b/lib/include/oclero/qlementine/widgets/Expander.hpp index 1722801..4d061e9 100644 --- a/lib/include/oclero/qlementine/widgets/Expander.hpp +++ b/lib/include/oclero/qlementine/widgets/Expander.hpp @@ -8,11 +8,13 @@ #include namespace oclero::qlementine { -/// A QWidget that allows to expand vertically, displaying its content. +/// A QWidget that allows to expand vertically or horizontally, +/// revealing or hiding its content with an animation. class Expander : public QWidget { Q_OBJECT Q_PROPERTY(bool expanded READ expanded WRITE setExpanded NOTIFY expandedChanged) + Q_PROPERTY(Qt::Orientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged) public: Expander(QWidget* parent = nullptr); @@ -20,6 +22,16 @@ class Expander : public QWidget { bool expanded() const; Q_SLOT void setExpanded(bool expanded); Q_SIGNAL void expandedChanged(); + void toggleExpanded(); + + Q_SIGNAL void aboutToExpand(); + Q_SIGNAL void aboutToShrink(); + Q_SIGNAL void didExpand(); + Q_SIGNAL void didShrink(); + + Qt::Orientation orientation() const; + Q_SLOT void setOrientation(Qt::Orientation orientation); + Q_SIGNAL void orientationChanged(); QWidget* content() const; void setContent(QWidget* content); @@ -37,6 +49,7 @@ class Expander : public QWidget { private: bool _expanded{ false }; + Qt::Orientation _orientation{ Qt::Orientation::Vertical }; QVariantAnimation _animation; QPointer _content{ nullptr }; }; diff --git a/lib/src/style/Delegates.cpp b/lib/src/style/Delegates.cpp index 3c3aa3f..863976d 100644 --- a/lib/src/style/Delegates.cpp +++ b/lib/src/style/Delegates.cpp @@ -21,8 +21,11 @@ void ComboBoxDelegate::paint(QPainter* p, const QStyleOptionViewItem& opt, const const auto& theme = _qlementineStyle ? _qlementineStyle->theme() : Theme{}; const auto isSeparator = idx.data(Qt::AccessibleDescriptionRole).toString() == QLatin1String("separator"); + const auto contentMargin = _qlementineStyle->pixelMetric(QStyle::PM_MenuHMargin); + const auto contentRect = opt.rect.marginsRemoved({ contentMargin, 0, contentMargin, 0 }); + if (isSeparator) { - const auto& rect = opt.rect; + const auto& rect = contentRect; const auto& color = _qlementineStyle ? _qlementineStyle->toolBarSeparatorColor() : Theme().secondaryAlternativeColorDisabled; const auto lineW = theme.borderWidth; @@ -37,11 +40,11 @@ void ComboBoxDelegate::paint(QPainter* p, const QStyleOptionViewItem& opt, const const auto mouse = getMenuItemMouseState(opt.state); // Background. + const auto& bgRect = contentRect; const auto hPadding = theme.spacing; - const auto& bgRect = opt.rect; const auto& bgColor = _qlementineStyle ? _qlementineStyle->menuItemBackgroundColor(mouse) : Theme().primaryColorTransparent; - constexpr auto radius = 0; + const auto radius = _qlementineStyle->theme().borderRadius - contentMargin / 2; p->setRenderHint(QPainter::Antialiasing, true); p->setPen(Qt::NoPen); p->setBrush(bgColor); @@ -128,8 +131,9 @@ QSize ComboBoxDelegate::sizeHint(const QStyleOptionViewItem& opt, const QModelIn const auto h = theme.spacing + theme.borderWidth; return QSize{ h, h }; } else { + const auto contentMargin = _qlementineStyle->pixelMetric(QStyle::PM_MenuHMargin); const auto hPadding = theme.spacing; - const auto vPadding = theme.spacing / 2; + const auto vPadding = theme.spacing; const auto iconSize = theme.iconSize; const auto spacing = theme.spacing; const auto& fm = opt.fontMetrics; @@ -142,7 +146,7 @@ QSize ComboBoxDelegate::sizeHint(const QStyleOptionViewItem& opt, const QModelIn iconVariant.isValid() && iconVariant.userType() == QMetaType::QIcon ? iconVariant.value() : QIcon{}; const auto textW = qlementine::textWidth(fm, text); const auto iconW = !icon.isNull() ? iconSize.width() + spacing : 0; - const auto w = std::max(0, hPadding + iconW + textW + hPadding); + const auto w = std::max(0, contentMargin * 2 + hPadding + iconW + textW + hPadding); const auto h = std::max(theme.controlHeightMedium, std::max(iconSize.height(), vPadding) + vPadding); return QSize{ w, h }; } diff --git a/lib/src/style/EventFilters.cpp b/lib/src/style/EventFilters.cpp index deb0859..e904bc3 100644 --- a/lib/src/style/EventFilters.cpp +++ b/lib/src/style/EventFilters.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include @@ -16,10 +18,15 @@ #include #include #include +#include +#include +#include +#include +#include namespace oclero::qlementine { LineEditButtonEventFilter::LineEditButtonEventFilter( - QlementineStyle& style, WidgetAnimationManager& animManager, QToolButton* button) + QlementineStyle* style, WidgetAnimationManager& animManager, QToolButton* button) : QObject(button) , _style(style) , _animManager(animManager) @@ -44,7 +51,7 @@ bool LineEditButtonEventFilter::eventFilter(QObject* watchedObject, QEvent* evt) // Instead, place the button by ourselves. const auto* parentLineEdit = _button->parentWidget(); const auto parentRect = parentLineEdit->rect(); - const auto& theme = _style.theme(); + const auto& theme = _style ? _style->theme() : Theme{}; const auto buttonH = theme.controlHeightMedium; const auto buttonW = buttonH; const auto spacing = theme.spacing / 2; @@ -63,29 +70,46 @@ bool LineEditButtonEventFilter::eventFilter(QObject* watchedObject, QEvent* evt) const auto hovered = _button->underMouse(); const auto pressed = _button->isDown(); const auto mouse = getMouseState(pressed, hovered, enabled); - const auto& theme = _style.theme(); + const auto& theme = _style ? _style->theme() : Theme{}; const auto rect = _button->rect(); - const auto& bgColor = _style.toolButtonBackgroundColor(mouse, ColorRole::Secondary); + + const auto& bgColor = + _style ? _style->toolButtonBackgroundColor(mouse, ColorRole::Secondary) + : _button->style()->standardPalette().color(getPaletteColorGroup(mouse), QPalette::ColorRole::ButtonText); + const auto& fgColor = + _style ? _style->toolButtonForegroundColor(mouse, ColorRole::Secondary) + : _button->style()->standardPalette().color(getPaletteColorGroup(mouse), QPalette::ColorRole::Button); + const auto animationDuration = _style ? _style->theme().animationDuration : 0; + const auto& currentBgColor = _animManager.animateBackgroundColor(_button, bgColor, animationDuration); + const auto& currentFgColor = _animManager.animateForegroundColor(_button, fgColor, animationDuration); + + // Get opacity animated in qlinedit_p.cpp:436 + const auto opacity = _button->property(QByteArrayLiteral("opacity")).toDouble(); + const auto circleH = theme.controlHeightMedium; const auto circleW = circleH; const auto circleX = rect.x() + (rect.width() - circleW) / 2; const auto circleY = rect.y() + (rect.height() - circleH) / 2; const auto circleRect = QRect(QPoint{ circleX, circleY }, QSize{ circleW, circleH }); - // Get opacity animated in qlinedit_p.cpp:436 - const auto opacity = _button->property(QByteArrayLiteral("opacity")).toDouble(); + const auto pixmap = getPixmap(_button->icon(), theme.iconSize, mouse, CheckState::NotChecked, _button); + const auto autoIconColor = _style ? _style->autoIconColor(_button) : AutoIconColor::None; + const auto& colorizedPixmap = _style->getColorizedPixmap(pixmap, autoIconColor, currentFgColor, currentFgColor); const auto pixmapX = circleRect.x() + (circleRect.width() - theme.iconSize.width()) / 2; const auto pixmapY = circleRect.y() + (circleRect.height() - theme.iconSize.height()) / 2; const auto pixmapRect = QRect{ { pixmapX, pixmapY }, theme.iconSize }; - const auto& currentBgColor = _animManager.animateBackgroundColor(_button, bgColor, theme.animationDuration); QPainter p(_button); p.setOpacity(opacity); p.setPen(Qt::NoPen); - p.setBrush(currentBgColor); p.setRenderHint(QPainter::Antialiasing, true); + + // Background. + p.setBrush(currentBgColor); p.drawEllipse(circleRect); - p.drawPixmap(pixmapRect, pixmap); + + // Foreground. + p.drawPixmap(pixmapRect, colorizedPixmap); evt->accept(); return true; @@ -231,12 +255,20 @@ bool TabBarEventFilter::eventFilter(QObject* watchedObject, QEvent* evt) { _tabBar->setIconSize(_tabBar->iconSize()); } else if (type == QEvent::Wheel) { const auto* wheelEvent = static_cast(evt); + + // Block non-horizontal scorll. + const bool wheelVertical = qAbs(wheelEvent->angleDelta().y()) > qAbs(wheelEvent->angleDelta().x()); + if (wheelVertical) { + evt->ignore(); + return true; + } + auto delta = wheelEvent->pixelDelta().x(); // If delta is null, it might be because we are on MacOS, using a trackpad. // So let's use angleDelta instead. if (delta == 0) { - delta = wheelEvent->angleDelta().y(); + delta = wheelEvent->angleDelta().x(); } // Invert the value if necessary. @@ -277,53 +309,143 @@ MenuEventFilter::MenuEventFilter(QMenu* menu) bool MenuEventFilter::eventFilter(QObject* watchedObject, QEvent* evt) { const auto type = evt->type(); - if (type == QEvent::Type::Show) { - // Place the QMenu correctly by making up for the drop shadow margins. - // It'll be reset before every show, so we can safely move it every time. - // Submenus should already be placed correctly, so there's no need to translate their geometry. - // Also, make up for the menu item padding so the texts are aligned. - const auto isMenuBarMenu = qobject_cast(_menu->parentWidget()) != nullptr; - const auto isSubMenu = qobject_cast(_menu->parentWidget()) != nullptr; - const auto alignForMenuBar = isMenuBarMenu && !isSubMenu; - const auto* qlementineStyle = qobject_cast(_menu->style()); - const auto menuItemHPadding = qlementineStyle ? qlementineStyle->theme().spacing : 0; - const auto menuDropShadowWidth = qlementineStyle ? qlementineStyle->theme().spacing : 0; - const auto menuOriginalPos = _menu->pos(); - const auto menuBarTranslation = alignForMenuBar ? QPoint(-menuItemHPadding, 0) : QPoint(0, 0); - const auto shadowTranslation = QPoint(-menuDropShadowWidth, -menuDropShadowWidth); - const auto menuNewPos = menuOriginalPos + menuBarTranslation + shadowTranslation; - - // Menus have weird sizing bugs when moving them from this event. - // We have to wait for the event loop to be processed before setting the final position. - const auto menuSize = _menu->size(); - if (menuSize != QSize(0, 0)) { - _menu->resize(0, 0); // Hide the menu for now until we can set the position. - QTimer::singleShot(0, _menu, [this, menuNewPos, menuSize]() { - _menu->move(menuNewPos); - _menu->resize(menuSize); - }); - } + + switch (type) { + case QEvent::Type::Show: { + // Place the QMenu correctly by making up for the drop shadow margins. + // It'll be reset before every show, so we can safely move it every time. + // Submenus should already be placed correctly, so there's no need to translate their geometry. + // Also, make up for the menu item padding so the texts are aligned. + const auto isMenuBarMenu = qobject_cast(_menu->parentWidget()) != nullptr; + const auto isSubMenu = qobject_cast(_menu->parentWidget()) != nullptr; + const auto alignForMenuBar = isMenuBarMenu && !isSubMenu; + const auto* qlementineStyle = qobject_cast(_menu->style()); + const auto menuItemHPadding = qlementineStyle ? qlementineStyle->theme().spacing : 0; + const auto menuDropShadowWidth = qlementineStyle ? qlementineStyle->theme().spacing : 0; + const auto menuOriginalPos = _menu->pos(); + const auto menuBarTranslation = alignForMenuBar ? QPoint(-menuItemHPadding, 0) : QPoint(0, 0); + const auto shadowTranslation = QPoint(-menuDropShadowWidth, -menuDropShadowWidth); + const auto menuNewPos = menuOriginalPos + menuBarTranslation + shadowTranslation; + + // Menus have weird sizing bugs when moving them from this event. + // We have to wait for the event loop to be processed before setting the final position. + const auto menuSize = _menu->size(); + if (menuSize != QSize(0, 0)) { + _menu->resize(0, 0); // Hide the menu for now until we can set the position. + QTimer::singleShot(0, _menu, [this, menuNewPos, menuSize]() { + _menu->move(menuNewPos); + _menu->resize(menuSize); + }); + } + } break; + case QEvent::Type::MouseButtonPress: { + const auto* mouseEvt = static_cast(evt); + const auto mousePos = mouseEvt->pos(); + if (const auto* action = _menu->actionAt(mousePos)) { + if (action->isSeparator() || !action->isEnabled() || action->property("qlementine_flashing").toBool()) { + return true; + } + } else if (_menu->rect().contains(mousePos)) { + return true; + } + } break; + case QEvent::Type::MouseButtonRelease: { + const auto* mouseEvt = static_cast(evt); + const auto mousePos = mouseEvt->pos(); + if (auto* action = _menu->actionAt(mousePos)) { + if (action->isSeparator() || !action->isEnabled() || action->property("qlementine_flashing").toBool()) + return true; + + if (action->menu() == nullptr) { + flashAction(action, _menu, [this, action]() { + // The call to QAction::trigger might destroy the menu or the actions. + const QPointer menu_guard(_menu); + action->trigger(); + if (menu_guard) { + if (auto* top_menu = getTopLevelMenu(menu_guard)) { + top_menu->close(); + } + } + }); + return true; + } + } else if (_menu->rect().contains(mousePos)) { + return true; + } + } break; + default: + break; } return QObject::eventFilter(watchedObject, evt); } -ComboboxItemViewFilter::ComboboxItemViewFilter(QAbstractItemView* view) +ComboboxItemViewFilter::ComboboxItemViewFilter(QComboBox* comboBox, QListView* view) : QObject(view) + , _comboBox(comboBox) , _view(view) { - view->installEventFilter(this); + _view->installEventFilter(this); + + auto* comboBoxPopup = _view->parentWidget(); + comboBoxPopup->installEventFilter(this); + + const auto childWidgets = comboBoxPopup->findChildren(); + for (auto* child : childWidgets) { + if (child->inherits("QComboBoxPrivateScroller")) { + child->setFixedHeight(0); + child->setVisible(false); + } + } + + _comboBox->installEventFilter(this); } bool ComboboxItemViewFilter::eventFilter(QObject* watchedObject, QEvent* evt) { const auto type = evt->type(); - if (type == QEvent::Type::Show) { - // Fix Qt bug. - const auto width = _view->sizeHintForColumn(0); - _view->setMinimumWidth(width); + switch (type) { + case QEvent::Type::Show: + fixViewGeometry(); + break; + case QEvent::Type::Resize: + if (watchedObject == _comboBox) { + fixViewGeometry(); + } + break; + default: + break; } return QObject::eventFilter(watchedObject, evt); } +void ComboboxItemViewFilter::fixViewGeometry() { + const auto* comboBox = findFirstParentOfType(_view); + const auto* qlementineStyle = qobject_cast(comboBox->style()); + const auto hMargin = qlementineStyle->pixelMetric(QStyle::PM_MenuHMargin); + const auto shadowWidth = qlementineStyle->theme().spacing; + const auto borderWidth = qlementineStyle->theme().borderWidth; + const auto width = + std::max(comboBox->width(), _view->sizeHintForColumn(0) + shadowWidth * 2) + hMargin * 2 + borderWidth * 2; + const auto height = viewMinimumSizeHint().height(); + _view->setFixedWidth(width); + _view->setFixedHeight(height); + _view->parentWidget()->adjustSize(); +} + +QSize ComboboxItemViewFilter::viewMinimumSizeHint() const { + // QListView::minimumSizeHint() doesn't give the correct minimumHeight, + // so we have to compute it. + const auto rowCount = _view->model()->rowCount(); + const auto maxHeight = _view->maximumHeight(); + auto height = 0; + for (auto i = 0; i < rowCount && height <= maxHeight; ++i) { + const auto rowSizeHint = _view->sizeHintForRow(i); + height = std::min(maxHeight, height + rowSizeHint); + } + // It looks like it is OK for the width, though. + const auto width = _view->sizeHintForColumn(0); + return { width, height }; +} + TextEditEventFilter::TextEditEventFilter(QAbstractScrollArea* textEdit) : QObject(textEdit) , _textEdit(textEdit) {} @@ -367,10 +489,9 @@ bool TextEditEventFilter::eventFilter(QObject* watchedObject, QEvent* evt) { return QObject::eventFilter(watchedObject, evt); } -WidgetWithFocusFrameEventFilter::WidgetWithFocusFrameEventFilter(QWidget* widget): - QObject(widget), - _widget(widget) { -} +WidgetWithFocusFrameEventFilter::WidgetWithFocusFrameEventFilter(QWidget* widget) + : QObject(widget) + , _widget(widget) {} bool WidgetWithFocusFrameEventFilter::eventFilter(QObject* watchedObject, QEvent* evt) { if (watchedObject == _widget) { @@ -387,4 +508,127 @@ bool WidgetWithFocusFrameEventFilter::eventFilter(QObject* watchedObject, QEvent return QObject::eventFilter(watchedObject, evt); } +class LineEditMenuIconsBehavior : public QObject { + QPointer _menu{ nullptr }; + bool _menuCustomized{ false }; + + enum class IconListMode { + None, + LineEdit, + ReadOnlyLineEdit, + SpinBox, + }; + + static std::vector iconList(IconListMode mode) { + const auto* qlem = oclero::qlementine::appStyle(); + if (!qlem) + return {}; + + // The order follows the one defined QLineEdit.cpp and QSpinBox.cpp (Qt6). + switch (mode) { + case IconListMode::LineEdit: + return { + QIcon(), // Separator + qlem->makeThemedIconFromName("edit-undo"), + qlem->makeThemedIconFromName("edit-redo"), + QIcon(), // Separator + qlem->makeThemedIconFromName("edit-cut"), + qlem->makeThemedIconFromName("edit-copy"), + qlem->makeThemedIconFromName("edit-paste"), + qlem->makeThemedIconFromName("edit-delete"), + QIcon(), // Separator + qlem->makeThemedIconFromName("edit-select-all"), + }; + case IconListMode::ReadOnlyLineEdit: + return { + QIcon(), // Separator + qlem->makeThemedIconFromName("edit-copy"), + QIcon(), // Separator + qlem->makeThemedIconFromName("edit-select-all"), + }; + case IconListMode::SpinBox: + return { + QIcon(), // Separator + qlem->makeThemedIconFromName("edit-undo"), + qlem->makeThemedIconFromName("edit-redo"), + QIcon(), // Separator + qlem->makeThemedIconFromName("edit-cut"), + qlem->makeThemedIconFromName("edit-copy"), + qlem->makeThemedIconFromName("edit-paste"), + qlem->makeThemedIconFromName("edit-delete"), + QIcon(), // Separator + QIcon(), // Separator + qlem->makeThemedIconFromName("edit-select-all"), + QIcon(), // Separator + qlem->makeThemedIconFromName("go-up"), + qlem->makeThemedIconFromName("go-down"), + }; + default: + return {}; + } + } + + static IconListMode getMode(const QMenu* menu) { + if (const auto* menu_parent = menu->parent()) { + if (qobject_cast(menu_parent->parent())) { + return IconListMode::SpinBox; + } else if (const auto* line_edit = qobject_cast(menu_parent)) { + return line_edit->isReadOnly() ? IconListMode::ReadOnlyLineEdit : IconListMode::LineEdit; + } + } + return IconListMode::None; + } + + void customizeMenu() { + const auto actions = _menu->findChildren(); + if (!actions.empty()) { + const auto icons = iconList(getMode(_menu)); + if (!icons.empty()) { + for (auto i = 0; i < static_cast(icons.size()) && i < static_cast(actions.size()); ++i) { + if (auto* action = actions.at(i)) { + action->setIcon(icons.at(i)); + } + } + } + } + _menu->adjustSize(); + } + +public: + LineEditMenuIconsBehavior(QMenu* menu) + : QObject(menu) + , _menu(menu) { + // Hack pour modifier les icones du menu contextuel des line edit. + QObject::connect(_menu, &QMenu::aboutToShow, this, [this]() { + if (!_menuCustomized) { + customizeMenu(); + _menuCustomized = true; + } + }); + } +}; + +LineEditMenuEventFilter::LineEditMenuEventFilter(QWidget* parent) + : QObject(parent) { + assert(parent); + if (auto* menu = qobject_cast(parent)) { + new LineEditMenuIconsBehavior(menu); + } else { + parent->installEventFilter(this); + } +} + +bool LineEditMenuEventFilter::eventFilter(QObject*, QEvent* evt) { + const auto type = evt->type(); + if (type == QEvent::ChildPolished) { + auto* child = static_cast(evt)->child(); + if (auto* lineedit = qobject_cast(child)) { + lineedit->installEventFilter(this); + } else if (auto* menu = qobject_cast(child)) { + new LineEditMenuIconsBehavior(menu); + } + } + + return false; +} } // namespace oclero::qlementine diff --git a/lib/src/style/EventFilters.hpp b/lib/src/style/EventFilters.hpp index 2058eb5..a863647 100644 --- a/lib/src/style/EventFilters.hpp +++ b/lib/src/style/EventFilters.hpp @@ -8,20 +8,22 @@ #include #include -#include +#include +#include #include +#include class QFocusFrame; namespace oclero::qlementine { class LineEditButtonEventFilter : public QObject { public: - LineEditButtonEventFilter(QlementineStyle& style, WidgetAnimationManager& animManager, QToolButton* button); + LineEditButtonEventFilter(QlementineStyle* style, WidgetAnimationManager& animManager, QToolButton* button); bool eventFilter(QObject* watchedObject, QEvent* evt) override; private: - QlementineStyle& _style; + QPointer _style; WidgetAnimationManager& _animManager; QToolButton* _button{ nullptr }; }; @@ -73,12 +75,15 @@ class MenuEventFilter : public QObject { class ComboboxItemViewFilter : public QObject { public: - ComboboxItemViewFilter(QAbstractItemView* view); + ComboboxItemViewFilter(QComboBox* comboBox, QListView* view); bool eventFilter(QObject* watchedObject, QEvent* evt) override; private: - QAbstractItemView* _view{ nullptr }; + void fixViewGeometry(); + QSize viewMinimumSizeHint() const; + QComboBox* _comboBox{ nullptr }; + QListView* _view{ nullptr }; }; // Works for both QTextEdit and QPlainTextEdit @@ -104,4 +109,11 @@ class WidgetWithFocusFrameEventFilter : public QObject { QFocusFrame* _focusFrame{ nullptr }; }; +class LineEditMenuEventFilter : public QObject { +public: + LineEditMenuEventFilter(QWidget* parent); + +protected: + virtual bool eventFilter(QObject* obj, QEvent* evt) override; +}; } // namespace oclero::qlementine diff --git a/lib/src/style/QlementineStyle.cpp b/lib/src/style/QlementineStyle.cpp index 855c996..a2ad7d5 100644 --- a/lib/src/style/QlementineStyle.cpp +++ b/lib/src/style/QlementineStyle.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -52,11 +53,18 @@ #include #include #include +#include +#include #include #include namespace oclero::qlementine { + +QlementineStyle* appStyle() { + return qobject_cast(qApp->style()); +} + /// Used to initializeResources from .qrc only once. std::once_flag qlementineOnceFlag; @@ -211,20 +219,42 @@ struct QlementineStyleImpl { return QMargins(paddingLeft, paddingTop, paddingRight, paddingBottom); } + /// Makes an IconTheme from the Theme. + IconTheme iconThemeFromTheme(ColorRole role = ColorRole::Secondary) const { + switch (role) { + case ColorRole::Primary: + return { + owner.iconForegroundColor(MouseState::Normal, ColorRole::Primary), + owner.iconForegroundColor(MouseState::Hovered, ColorRole::Primary), + owner.iconForegroundColor(MouseState::Pressed, ColorRole::Primary), + owner.iconForegroundColor(MouseState::Disabled, ColorRole::Primary), + }; + case ColorRole::Secondary: + default: + return { + owner.iconForegroundColor(MouseState::Normal, ColorRole::Secondary), + owner.iconForegroundColor(MouseState::Hovered, ColorRole::Secondary), + owner.iconForegroundColor(MouseState::Pressed, ColorRole::Secondary), + owner.iconForegroundColor(MouseState::Disabled, ColorRole::Secondary), + }; + } + } + QlementineStyle& owner; - Theme theme; + Theme theme{}; std::unique_ptr fontMetricsBold{ nullptr }; WidgetAnimationManager animations; std::unordered_map standardIconCache; std::unordered_map standardIconExtCache; - bool useMenuForComboBoxPopup{ false }; AutoIconColor autoIconColor{ AutoIconColor::None }; + std::function iconPathFunc; }; QlementineStyle::QlementineStyle(QObject* parent) : _impl(new QlementineStyleImpl{ *this }) { setParent(parent); setObjectName(QStringLiteral("QlementineStyle")); + triggerCompleteRepaint(); } QlementineStyle::~QlementineStyle() = default; @@ -243,8 +273,10 @@ void QlementineStyle::setTheme(Theme const& theme) { } void QlementineStyle::setThemeJsonPath(QString const& jsonPath) { - const auto theme = Theme(jsonPath); - setTheme(theme); + const auto themeOpt = Theme::fromJsonPath(jsonPath); + if (themeOpt.has_value()) { + setTheme(themeOpt.value()); + } } bool QlementineStyle::animationsEnabled() const { @@ -259,17 +291,6 @@ void QlementineStyle::setAnimationsEnabled(bool enabled) { } } -bool QlementineStyle::useMenuForComboBoxPopup() const { - return _impl->useMenuForComboBoxPopup; -} - -void QlementineStyle::setUseMenuForComboBoxPopup(bool useMenu) { - if (useMenu != _impl->useMenuForComboBoxPopup) { - _impl->useMenuForComboBoxPopup = useMenu; - emit useMenuForComboBoxPopupChanged(); - } -} - void QlementineStyle::triggerCompleteRepaint() { _impl->updateFonts(); _impl->updatePalette(); @@ -336,21 +357,22 @@ QPixmap QlementineStyle::getColorizedPixmap( return input; } -QIcon QlementineStyle::makeIcon(const QString& svgPath) { - QIcon result; - QPixmap pixmap(svgPath); - - result.addPixmap(pixmap, QIcon::Normal, QIcon::Off); - result.addPixmap(pixmap, QIcon::Disabled, QIcon::Off); - result.addPixmap(pixmap, QIcon::Active, QIcon::Off); - result.addPixmap(pixmap, QIcon::Selected, QIcon::Off); +QIcon QlementineStyle::makeThemedIcon(const QString& svgPath, const QSize& size, ColorRole role) const { + const auto iconTheme = _impl->iconThemeFromTheme(role); + return makeIconFromSvg(svgPath, iconTheme, size); +} - result.addPixmap(pixmap, QIcon::Normal, QIcon::On); - result.addPixmap(pixmap, QIcon::Disabled, QIcon::On); - result.addPixmap(pixmap, QIcon::Active, QIcon::On); - result.addPixmap(pixmap, QIcon::Selected, QIcon::On); +QIcon QlementineStyle::makeThemedIconFromName(const QString& name, const QSize& size, ColorRole role) const { + if (_impl->iconPathFunc) { + const auto iconPath = _impl->iconPathFunc(name); + return makeThemedIcon(iconPath, size, role); + } else { + return QIcon::fromTheme(name); + } +} - return result; +void QlementineStyle::setIconPathGetter(const std::function& func) { + _impl->iconPathFunc = func; } /* QStyle overrides. */ @@ -405,8 +427,17 @@ void QlementineStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption* opt break; case PE_FrameMenu: return; // Let PE_PanelMenu do the drawing. - case PE_FrameStatusBarItem: - break; + case PE_FrameStatusBarItem: { + const auto rect = opt->rect; + const auto penColor = _impl->theme.borderColor; + const auto penWidth = _impl->theme.borderWidth; + const auto p1 = QPoint{ rect.x() + 1 + penWidth, rect.y() + rect.x() }; + const auto p2 = QPoint{ rect.x() + 1 + penWidth, rect.y() + rect.height() }; + p->setPen(QPen(penColor, penWidth, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin)); + p->setBrush(Qt::NoBrush); + p->drawLine(p1, p2); + } + return; case PE_FrameTabWidget: { // QTabWidget.cpp, line 1296, in QTabWidget::paintEvent(): // The widget does not draw the Tab bar background unless it's in @@ -417,8 +448,9 @@ void QlementineStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption* opt const auto* tabBar = tabWidget ? tabWidget->tabBar() : nullptr; if (!documentMode && tabBar) { // Draw a border around the content. + const auto mouse = getMouseState(opt->state); const auto radius = _impl->theme.borderRadius * 1.5; - const auto& borderColor = tabBarBackgroundColor(); + const auto borderColor = tabBarBackgroundColor(mouse); const auto borderW = _impl->theme.borderWidth; drawRoundedRectBorder( p, opt->rect.adjusted(0, -borderW, 0, 0), borderColor, borderW, RadiusesF(0., 0., radius, radius)); @@ -458,11 +490,13 @@ void QlementineStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption* opt } case PE_FrameTabBarBase: if (const auto* optTabBar = qstyleoption_cast(opt)) { + const auto mouse = getMouseState(opt->state); + const auto& bgColor = tabBarBackgroundColor(mouse); if (optTabBar->documentMode) { - p->fillRect(opt->rect, tabBarBackgroundColor()); + p->fillRect(opt->rect, bgColor); } else { const auto radius = _impl->theme.borderRadius * 1.5; - drawRoundedRect(p, opt->rect, tabBarBackgroundColor(), RadiusesF(radius, radius, 0., 0.)); + drawRoundedRect(p, opt->rect, bgColor, RadiusesF(radius, radius, 0., 0.)); } } return; @@ -798,7 +832,8 @@ void QlementineStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption* opt // Filled rectangle below scroll buttons. // We need to fill the whole surface to ensure tabs are not visible below. - const auto& tabBarBgColor = tabBarBackgroundColor(); + const auto mouse = getMouseState(opt->state); + const auto& tabBarBgColor = tabBarBackgroundColor(mouse); const auto filledRect = QRect(rect.x() + rect.width() - scrollButtonsW, rect.y(), scrollButtonsW, rect.height()); drawRoundedRect(p, filledRect, tabBarBgColor, documentMode ? 0. : RadiusesF(0., radius, 0., 0.)); } @@ -1399,6 +1434,7 @@ void QlementineStyle::drawControl(ControlElement ce, const QStyleOption* opt, QP } // Icon. + const auto iconSpace = optMenuItem->maxIconWidth > 0 ? optMenuItem->maxIconWidth + spacing : 0; const auto pixmap = getPixmap(optMenuItem->icon, _impl->theme.iconSize, mouse, checkState, w); if (!pixmap.isNull()) { const auto& colorizedPixmap = getColorizedPixmap(pixmap, autoIconColor(w), fgColor, fgColor); @@ -1409,11 +1445,9 @@ void QlementineStyle::drawControl(ControlElement ce, const QStyleOption* opt, QP const auto pixmapY = fgRect.y() + (fgRect.height() - pixmapH) / 2; const auto pixmapRect = QRect{ pixmapX, pixmapY, pixmapW, pixmapH }; p->drawPixmap(pixmapRect, colorizedPixmap); - - const auto taken = pixmapW + spacing; - availableW -= taken; - availableX += taken; } + availableW -= iconSpace; + availableX += iconSpace; // Shortcut text. if (!shortcut.isEmpty()) { @@ -1789,33 +1823,39 @@ void QlementineStyle::drawControl(ControlElement ce, const QStyleOption* opt, QP break; case CE_SizeGrip: break; - case CE_Splitter: - break; - case CE_RubberBand: - break; - case CE_DockWidgetTitle: - break; - case CE_ScrollBarAddLine: - // TODO - break; - case CE_ScrollBarSubLine: - // TODO - break; - case CE_ScrollBarAddPage: - // TODO - break; - case CE_ScrollBarSubPage: - // TODO - break; - case CE_ScrollBarSlider: - // TODO - break; - case CE_ScrollBarFirst: - // TODO - break; - case CE_ScrollBarLast: - // TODO - break; + case CE_Splitter: { + const auto mouse = getMouseState(opt->state); + const auto& lineColor = splitterColor(mouse); + // const auto currentLineColor = _impl->animations.animateBackgroundColor(w, lineColor, _impl->theme.animationDuration); + const auto line_rect = opt->rect.adjusted(-1, 0, 1, 0); + p->fillRect(line_rect, lineColor); + } + return; + // case CE_RubberBand: + // break; + // case CE_DockWidgetTitle: + // break; + // case CE_ScrollBarAddLine: + // // TODO + // break; + // case CE_ScrollBarSubLine: + // // TODO + // break; + // case CE_ScrollBarAddPage: + // // TODO + // break; + // case CE_ScrollBarSubPage: + // // TODO + // break; + // case CE_ScrollBarSlider: + // // TODO + // break; + // case CE_ScrollBarFirst: + // // TODO + // break; + // case CE_ScrollBarLast: + // // TODO + // break; case CE_FocusFrame: if (const auto* focusFrame = qobject_cast(w)) { const auto* monitoredWidget = focusFrame->widget(); @@ -2828,11 +2868,13 @@ void QlementineStyle::drawComplexControl( // Draw an opaque background to hide tabs below. const auto isLeftButton = toolbuttonOpt->arrowType == Qt::ArrowType::LeftArrow; + const auto tabBarState = parentTabBar->isEnabled() ? MouseState::Normal : MouseState::Disabled; if (parentTabBar->documentMode() || isLeftButton) { - p->fillRect(toolbuttonOpt->rect, tabBarBackgroundColor()); + p->fillRect(toolbuttonOpt->rect, tabBarBackgroundColor(tabBarState)); } else { const auto bgRadius = _impl->theme.borderRadius * 1.5; - drawRoundedRect(p, toolbuttonOpt->rect, tabBarBackgroundColor(), RadiusesF(0., bgRadius, 0., 0.)); + drawRoundedRect( + p, toolbuttonOpt->rect, tabBarBackgroundColor(tabBarState), RadiusesF(0., bgRadius, 0., 0.)); } // Rect. @@ -3261,7 +3303,9 @@ QRect QlementineStyle::subControlRect( if (comboBoxOpt->editable) { const auto indicatorSize = _impl->theme.iconSize; const auto spacing = _impl->theme.spacing; - if (qobject_cast(w) != nullptr) { + const auto isBasicComboBox = + qobject_cast(w) != nullptr && qobject_cast(w) == nullptr; + if (isBasicComboBox) { // Strange hack to place the QLineEdit correctly. const auto indicatorButtonW = spacing * 2 + indicatorSize.width(); const auto shiftX = static_cast(spacing * 2.5); @@ -3284,8 +3328,17 @@ QRect QlementineStyle::subControlRect( const auto frameY = comboBoxOpt->rect.y() + (comboBoxOpt->rect.height() - frameH) / 2; return QRect{ frameX, frameY, frameW, frameH }; } break; - case SC_ComboBoxListBoxPopup: - return opt->rect; + case SC_ComboBoxListBoxPopup: { + const auto contentMarginH = pixelMetric(PM_MenuHMargin); + const auto contentMarginV = pixelMetric(PM_MenuVMargin); + const auto shadowWidth = _impl->theme.spacing; + const auto borderWidth = _impl->theme.borderWidth; + const auto width = std::max(opt->rect.width(), w->width()); + const auto height = opt->rect.height() + 12; // Not possible to change height here. + const auto x = opt->rect.x() - shadowWidth - borderWidth - contentMarginH; + const auto y = opt->rect.y() - shadowWidth - borderWidth - contentMarginV / 2; // TODO remove hardcoded + return { x, y, width, height }; + } break; default: break; } @@ -3858,10 +3911,12 @@ QSize QlementineStyle::sizeFromContents( break; case CT_SpinBox: if (const auto* optSpinbox = qstyleoption_cast(opt)) { + const auto isDateTimeEdit = qobject_cast(widget) != nullptr; const auto hasButtons = optSpinbox->buttonSymbols != QAbstractSpinBox::NoButtons; - const auto buttonW = hasButtons ? _impl->theme.controlHeightLarge : 0; + const auto buttonW = isDateTimeEdit || hasButtons ? _impl->theme.controlHeightLarge : 0; + const auto dateTimeWidth = isDateTimeEdit ? _impl->theme.iconSize.width() : 0; const auto borderW = optSpinbox->frame ? pixelMetric(PM_SpinBoxFrameWidth, opt, widget) : 0; - return QSize{ contentSize.width() + buttonW + 2 * borderW, _impl->theme.controlHeightLarge }; + return QSize{ contentSize.width() + buttonW + dateTimeWidth + 2 * borderW, _impl->theme.controlHeightLarge }; } break; case CT_SizeGrip: @@ -4039,8 +4094,7 @@ int QlementineStyle::pixelMetric(PixelMetric m, const QStyleOption* opt, const Q // Splitter. case PM_SplitterWidth: - break; - + return 1; // TitleBar. case PM_TitleBarHeight: break; @@ -4054,9 +4108,11 @@ int QlementineStyle::pixelMetric(PixelMetric m, const QStyleOption* opt, const Q // Scroller is the part where the user can click to scroll the menu when it is too big. return _impl->theme.controlHeightSmall; case PM_MenuHMargin: - case PM_MenuVMargin: + case PM_MenuVMargin: { // Keep some space between the items and the frame. - return _impl->theme.spacing; + const auto borderW = qobject_cast(w) ? 1 : 0; + return _impl->theme.spacing / 2 + borderW; + } case PM_MenuPanelWidth: // Keep some space for drop shadow. return _impl->theme.spacing; @@ -4337,17 +4393,13 @@ int QlementineStyle::styleHint(StyleHint sh, const QStyleOption* opt, const QWid case SH_ComboBox_ListMouseTracking: return true; case SH_ComboBox_Popup: - // This changes the way the dropdown popup behaves. - // A different QItemDelegate will be used to size/draw the items. - // - true: not animated, uses QComboBoxMenuDelegate, that calls QStyle::drawControl(CE_MenuItem) - // - false: animated, uses QComboBoxDelegate, that just calls QItemDelegate::sizeHint()/paint() - return _impl->useMenuForComboBoxPopup; + return true; case SH_ComboBox_LayoutDirection: break; case SH_ComboBox_PopupFrameStyle: return QFrame::StyledPanel | QFrame::Plain; - case SH_ComboBox_UseNativePopup: // Only on MacOS. - return true; + case SH_ComboBox_UseNativePopup: + return false; case SH_ComboBox_AllowWheelScrolling: return false; @@ -4455,7 +4507,7 @@ int QlementineStyle::styleHint(StyleHint sh, const QStyleOption* opt, const QWid case SH_LineEdit_PasswordCharacter: return QChar(0x2022).unicode(); // Bullet. case SH_LineEdit_PasswordMaskDelay: - return 200; + return 0; // FocusFrame case SH_FocusFrame_AboveWidget: @@ -4530,144 +4582,6 @@ QIcon QlementineStyle::standardIcon(StandardPixmap sp, const QStyleOption* opt, case SP_ArrowRight: case SP_LineEditClearButton: return _impl->getStandardIcon(sp, _impl->theme.iconSize); - // case SP_TitleBarMenuButton: - // break; - // case SP_TitleBarMinButton: - // break; - // case SP_TitleBarMaxButton: - // break; - // case SP_TitleBarCloseButton: - // break; - // case SP_TitleBarNormalButton: - // break; - // case SP_TitleBarShadeButton: - // break; - // case SP_TitleBarUnshadeButton: - // break; - // case SP_TitleBarContextHelpButton: - // break; - // case SP_DockWidgetCloseButton: - // break; - // case SP_DesktopIcon: - // break; - // case SP_TrashIcon: - // break; - // case SP_ComputerIcon: - // break; - // case SP_DriveFDIcon: - // break; - // case SP_DriveHDIcon: - // break; - // case SP_DriveCDIcon: - // break; - // case SP_DriveDVDIcon: - // break; - // case SP_DriveNetIcon: - // break; - // case SP_DirOpenIcon: - // break; - // case SP_DirClosedIcon: - // break; - // case SP_DirLinkIcon: - // break; - // case SP_DirLinkOpenIcon: - // break; - // case SP_FileIcon: - // break; - // case SP_FileLinkIcon: - // break; - // case SP_FileDialogStart: - // break; - // case SP_FileDialogEnd: - // break; - // case SP_FileDialogToParent: - // break; - // case SP_FileDialogNewFolder: - // break; - // case SP_FileDialogDetailedView: - // break; - // case SP_FileDialogInfoView: - // break; - // case SP_FileDialogContentsView: - // break; - // case SP_FileDialogListView: - // break; - // case SP_FileDialogBack: - // break; - // case SP_DirIcon: - // break; - // case SP_DialogOkButton: - // break; - // case SP_DialogCancelButton: - // break; - // case SP_DialogHelpButton: - // break; - // case SP_DialogOpenButton: - // break; - // case SP_DialogSaveButton: - // break; - // case SP_DialogCloseButton: - // break; - // case SP_DialogApplyButton: - // break; - // case SP_DialogResetButton: - // break; - // case SP_DialogDiscardButton: - // break; - // case SP_DialogYesButton: - // break; - // case SP_DialogNoButton: - // break; - // case SP_DialogYesToAllButton: - // break; - // case SP_DialogNoToAllButton: - // break; - // case SP_DialogSaveAllButton: - // break; - // case SP_DialogAbortButton: - // break; - // case SP_DialogRetryButton: - // break; - // case SP_DialogIgnoreButton: - // break; - // case SP_ArrowUp: - // break; - // case SP_ArrowDown: - // break; - // case SP_ArrowBack: - // break; - // case SP_ArrowForward: - // break; - // case SP_DirHomeIcon: - // break; - // case SP_CommandLink: - // break; - // case SP_VistaShield: - // break; - // case SP_BrowserReload: - // break; - // case SP_BrowserStop: - // break; - // case SP_MediaPlay: - // break; - // case SP_MediaStop: - // break; - // case SP_MediaPause: - // break; - // case SP_MediaSkipForward: - // break; - // case SP_MediaSkipBackward: - // break; - // case SP_MediaSeekForward: - // break; - // case SP_MediaSeekBackward: - // break; - // case SP_MediaVolume: - // break; - // case SP_MediaVolumeMuted: - // break; - // case SP_RestoreDefaultsButton: - // break; default: break; } @@ -4700,6 +4614,8 @@ void QlementineStyle::polish(QApplication* app) { QCommonStyle::polish(app); app->setFont(_impl->theme.fontRegular); //app->installEventFilter(new AppEventFilter(app)); + + QApplication::setAttribute(Qt::ApplicationAttribute::AA_DontShowIconsInMenus, false); } void QlementineStyle::unpolish(QApplication* app) { @@ -4724,7 +4640,7 @@ void QlementineStyle::polish(QWidget* w) { // Special case for the Qt-private buttons in a QLineEdit. if (w->inherits("QLineEditIconButton")) { - w->installEventFilter(new LineEditButtonEventFilter(*this, _impl->animations, qobject_cast(w))); + w->installEventFilter(new LineEditButtonEventFilter(this, _impl->animations, qobject_cast(w))); w->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); // Fix hardcoded width in qlineedit_p.cpp:493 w->setFixedSize(_impl->theme.controlHeightMedium, _impl->theme.controlHeightMedium); @@ -4778,25 +4694,26 @@ void QlementineStyle::polish(QWidget* w) { } // Try to remove the background... - if (auto* itemView = qobject_cast(w)) { - auto* parent = itemView->parentWidget(); - auto isComboBoxPopupContainer = parent && parent->inherits("QComboBoxPrivateContainer"); + if (auto* itemView = qobject_cast(w)) { + auto* popup = itemView->parentWidget(); + auto isComboBoxPopupContainer = popup && popup->inherits("QComboBoxPrivateContainer"); if (isComboBoxPopupContainer) { - itemView->setBackgroundRole(QPalette::NoRole); - itemView->viewport()->setBackgroundRole(QPalette::NoRole); - parent->setBackgroundRole(QPalette::NoRole); - parent->setAutoFillBackground(false); - parent->setAttribute(Qt::WA_TranslucentBackground, true); - parent->setAttribute(Qt::WA_OpaquePaintEvent, false); - parent->setAttribute(Qt::WA_NoSystemBackground, true); - itemView->installEventFilter(new ComboboxItemViewFilter(itemView)); - if (auto* scrollArea = parent->findChild()) { - scrollArea->setBackgroundRole(QPalette::NoRole); - scrollArea->setAutoFillBackground(false); - scrollArea->setAttribute(Qt::WA_TranslucentBackground, true); - scrollArea->setAttribute(Qt::WA_OpaquePaintEvent, false); - scrollArea->setAttribute(Qt::WA_NoSystemBackground, true); - } + popup->setAttribute(Qt::WA_TranslucentBackground, true); + popup->setAttribute(Qt::WA_OpaquePaintEvent, false); + popup->setAttribute(Qt::WA_NoSystemBackground, true); + popup->setWindowFlag(Qt::FramelessWindowHint, true); + popup->setWindowFlag(Qt::NoDropShadowWindowHint, true); + popup->setProperty("_q_windowsDropShadow", false); + + // Same shadow as QMenu. + const auto shadowWidth = _impl->theme.spacing; + const auto borderWidth = _impl->theme.borderWidth; + const auto margin = shadowWidth + borderWidth; + popup->layout()->setContentsMargins(margin, margin, margin, margin); + + itemView->viewport()->setAutoFillBackground(false); + auto* comboBox = findFirstParentOfType(itemView); + itemView->installEventFilter(new ComboboxItemViewFilter(comboBox, itemView)); } } @@ -4861,6 +4778,12 @@ void QlementineStyle::polish(QWidget* w) { viewport->setAutoFillBackground(false); } } + + if (auto* lineEdit = qobject_cast(w)) { + lineEdit->installEventFilter(new LineEditMenuEventFilter(lineEdit)); + } else if (auto* spinBox = qobject_cast(w)) { + spinBox->installEventFilter(new LineEditMenuEventFilter(spinBox)); + } } void QlementineStyle::unpolish(QWidget* w) { @@ -5561,8 +5484,8 @@ QColor const& QlementineStyle::menuBarItemForegroundColor(MouseState const mouse } } -QColor const& QlementineStyle::tabBarBackgroundColor() const { - return _impl->theme.backgroundColorMain3; +QColor const& QlementineStyle::tabBarBackgroundColor(MouseState const mouse) const { + return mouse == MouseState::Disabled ? _impl->theme.backgroundColorMain3 : _impl->theme.backgroundColorTabBar; } QColor const& QlementineStyle::tabBarShadowColor() const { @@ -5575,18 +5498,21 @@ QColor const& QlementineStyle::tabBarBottomShadowColor() const { QColor const& QlementineStyle::tabBackgroundColor(MouseState const mouse, SelectionState const selected) const { const auto isSelected = selected == SelectionState::Selected; + const auto& selectedTabColor = _impl->theme.backgroundColorMain2; + const auto& hoverTabColor = _impl->theme.neutralColor; + const auto& defaultTabColor = _impl->theme.backgroundColorMainTransparent; switch (mouse) { case MouseState::Hovered: - return isSelected ? _impl->theme.backgroundColorMain2 : _impl->theme.neutralColorPressed; + return isSelected ? selectedTabColor : hoverTabColor; case MouseState::Pressed: - return isSelected ? _impl->theme.backgroundColorMain2 : _impl->theme.secondaryColorPressed; + return _impl->theme.backgroundColorMain2; case MouseState::Normal: - return isSelected ? _impl->theme.backgroundColorMain2 : _impl->theme.neutralColorTransparent; + return isSelected ? selectedTabColor : defaultTabColor; case MouseState::Disabled: case MouseState::Transparent: default: - return _impl->theme.neutralColorTransparent; + return defaultTabColor; } } @@ -5792,6 +5718,14 @@ QColor const& QlementineStyle::labelCaptionForegroundColor(MouseState const mous return _impl->theme.secondaryAlternativeColor; } +QColor const& QlementineStyle::iconForegroundColor(MouseState const mouse, ColorRole const role) const { + if (mouse == MouseState::Disabled) + return role == ColorRole::Primary ? _impl->theme.primaryColorForegroundDisabled + : _impl->theme.secondaryColorForegroundDisabled; + else + return role == ColorRole::Primary ? _impl->theme.primaryColorForeground : _impl->theme.secondaryColorForeground; +} + QColor const& QlementineStyle::toolBarBackgroundColor() const { return _impl->theme.backgroundColorMain2; } @@ -5855,8 +5789,13 @@ QColor const& QlementineStyle::groupBoxTitleColor(MouseState const mouse, const return labelForegroundColor(mouse, w); } -QColor const& QlementineStyle::groupBoxBackgroundColor(MouseState const mouse) const { - return mouse == MouseState::Disabled ? _impl->theme.neutralColorTransparent : _impl->theme.neutralColorDisabled; +QColor QlementineStyle::groupBoxBackgroundColor(MouseState const mouse) const { + if (mouse == MouseState::Disabled) { + return _impl->theme.backgroundColorMainTransparent; + } else { + return getColorSourceOver(_impl->theme.backgroundColorMain2, + colorWithAlphaF(_impl->theme.backgroundColorMain3, _impl->theme.backgroundColorMain3.alphaF() * .75)); + } } QColor const& QlementineStyle::groupBoxBorderColor(MouseState const mouse) const { @@ -6090,4 +6029,18 @@ QColor const& QlementineStyle::statusBarBorderColor() const { QColor const& QlementineStyle::statusBarSeparatorColor() const { return _impl->theme.secondaryColorDisabled; } + +QColor const& QlementineStyle::splitterColor(const MouseState mouse) const { + switch (mouse) { + case MouseState::Normal: + return _impl->theme.borderColor; + case MouseState::Hovered: + return _impl->theme.primaryColor; + case MouseState::Pressed: + return _impl->theme.primaryColorPressed; + case MouseState::Disabled: + default: + return _impl->theme.borderColorTransparent; + } +} } // namespace oclero::qlementine diff --git a/lib/src/style/Theme.cpp b/lib/src/style/Theme.cpp index 529363a..88496bf 100644 --- a/lib/src/style/Theme.cpp +++ b/lib/src/style/Theme.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -139,21 +140,39 @@ void setInt(QJsonObject& jsonObj, const QString& key, int value) { void setDouble(QJsonObject& jsonObj, const QString& key, double value) { jsonObj.insert(key, value); } -} // namespace - -Theme::Theme() - : Theme(QJsonDocument{}) {} -Theme::Theme(QString const& jsonPath) - : Theme(readJsonDoc(jsonPath)) {} +bool jsonObjHasKey(const QJsonObject& obj, const QString& key) { + return obj.find(key) != obj.end(); +} +bool jsonObjHasAllKeys(const QJsonObject& obj, const QVector& keys) { + for (const auto& key : keys) { + if (!jsonObjHasKey(obj, key)) + return false; + } + return true; +} +} // namespace -Theme::Theme(QJsonDocument const& jsonDoc) { - initializeFromJson(jsonDoc); +Theme::Theme() { initializeFonts(); initializePalette(); } +std::optional Theme::fromJsonPath(const QString& jsonPath) { + return fromJsonDoc(readJsonDoc(jsonPath)); +} + +std::optional Theme::fromJsonDoc(const QJsonDocument& jsonDoc) { + Theme theme; + if (theme.initializeFromJson(jsonDoc)) { + theme.initializeFonts(); + theme.initializePalette(); + return theme; + } + return std::nullopt; +} + void Theme::initializeFonts() { // Fonts. const auto defaultFont = QFont(QStringLiteral("Inter")); @@ -233,7 +252,7 @@ void Theme::initializePalette() { palette.setColor(QPalette::ColorGroup::All, QPalette::ColorRole::WindowText, secondaryColor); palette.setColor(QPalette::ColorGroup::Disabled, QPalette::ColorRole::WindowText, secondaryColorDisabled); palette.setColor(QPalette::ColorGroup::All, QPalette::ColorRole::PlaceholderText, secondaryColorDisabled); - palette.setColor(QPalette::ColorGroup::Disabled, QPalette::ColorRole::PlaceholderText, neutralColorDisabled); + palette.setColor(QPalette::ColorGroup::Disabled, QPalette::ColorRole::PlaceholderText, secondaryColorDisabled); palette.setColor(QPalette::ColorGroup::All, QPalette::ColorRole::Link, primaryColor); palette.setColor(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Link, secondaryColorDisabled); palette.setColor(QPalette::ColorGroup::All, QPalette::ColorRole::LinkVisited, primaryColor); @@ -251,160 +270,173 @@ void Theme::initializePalette() { palette.setColor(QPalette::ColorGroup::Disabled, QPalette::ColorRole::Button, neutralColorDisabled); } -void Theme::initializeFromJson(QJsonDocument const& jsonDoc) { - if (jsonDoc.isObject()) { - const auto jsonObj = jsonDoc.object(); - if (!jsonObj.isEmpty()) { - // Parse metadata. - auto const metaObj = jsonObj.value(QStringLiteral("meta")).toObject(); - meta = ThemeMeta{ - tryGetString(metaObj, QStringLiteral("name"), {}), - tryGetString(metaObj, QStringLiteral("version"), {}), - tryGetString(metaObj, QStringLiteral("author"), {}), - }; - - // Parse all values. - TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorMain1); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorMain2); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorMain3); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorMain4); - backgroundColorMainTransparent = colorWithAlpha(backgroundColorMain1, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, neutralColorDisabled); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, neutralColor); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, neutralColorHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, neutralColorPressed); - neutralColorTransparent = colorWithAlpha(neutralColorDisabled, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, focusColor); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColor); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorDisabled); - primaryColorTransparent = colorWithAlpha(primaryColor, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorForeground); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorForegroundHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorForegroundPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorForegroundDisabled); - primaryColorForegroundTransparent = colorWithAlpha(primaryColorForeground, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryAlternativeColor); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryAlternativeColorHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryAlternativeColorPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryAlternativeColorDisabled); - primaryAlternativeColorTransparent = colorWithAlpha(primaryAlternativeColor, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColor); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorDisabled); - secondaryColorTransparent = colorWithAlpha(secondaryColor, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryAlternativeColor); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryAlternativeColorHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryAlternativeColorPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryAlternativeColorDisabled); - secondaryAlternativeColorTransparent = colorWithAlpha(secondaryAlternativeColor, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorForeground); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorForegroundHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorForegroundPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorForegroundDisabled); - secondaryColorForegroundTransparent = colorWithAlpha(secondaryColorForeground, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, semiTransparentColor1); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, semiTransparentColor2); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, semiTransparentColor3); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, semiTransparentColor4); - semiTransparentColorTransparent = colorWithAlpha(semiTransparentColor1, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorSuccess); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorSuccessHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorSuccessPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorSuccessDisabled); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorInfo); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorInfoHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorInfoPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorInfoDisabled); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorWarning); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorWarningHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorWarningPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorWarningDisabled); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorError); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorErrorHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorErrorPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorErrorDisabled); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorForeground); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorForegroundHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorForegroundPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorForegroundDisabled); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, borderColor); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, borderColorHovered); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, borderColorPressed); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, borderColorDisabled); - borderColorTransparent = colorWithAlpha(borderColor, 0); - - TRY_GET_COLOR_ATTRIBUTE(jsonObj, shadowColor1); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, shadowColor2); - TRY_GET_COLOR_ATTRIBUTE(jsonObj, shadowColor3); - shadowColorTransparent = colorWithAlpha(shadowColor1, 0); - - TRY_GET_INT_ATTRIBUTE(jsonObj, fontSize); - TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH1); - TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH2); - TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH3); - TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH4); - TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH5); - TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeS1); - TRY_GET_INT_ATTRIBUTE(jsonObj, animationDuration); - TRY_GET_INT_ATTRIBUTE(jsonObj, focusAnimationDuration); - TRY_GET_INT_ATTRIBUTE(jsonObj, sliderAnimationDuration); - TRY_GET_DOUBLE_ATTRIBUTE(jsonObj, borderRadius); - TRY_GET_DOUBLE_ATTRIBUTE(jsonObj, checkBoxBorderRadius); - TRY_GET_DOUBLE_ATTRIBUTE(jsonObj, menuItemBorderRadius); - TRY_GET_DOUBLE_ATTRIBUTE(jsonObj, menuBarItemBorderRadius); - TRY_GET_INT_ATTRIBUTE(jsonObj, borderWidth); - TRY_GET_INT_ATTRIBUTE(jsonObj, controlHeightLarge); - TRY_GET_INT_ATTRIBUTE(jsonObj, controlHeightMedium); - TRY_GET_INT_ATTRIBUTE(jsonObj, controlHeightSmall); - TRY_GET_INT_ATTRIBUTE(jsonObj, controlDefaultWidth); - TRY_GET_INT_ATTRIBUTE(jsonObj, dialMarkLength); - TRY_GET_INT_ATTRIBUTE(jsonObj, dialMarkThickness); - TRY_GET_INT_ATTRIBUTE(jsonObj, dialTickLength); - TRY_GET_INT_ATTRIBUTE(jsonObj, dialTickSpacing); - TRY_GET_INT_ATTRIBUTE(jsonObj, dialGrooveThickness); - TRY_GET_INT_ATTRIBUTE(jsonObj, focusBorderWidth); - - auto iconExtent = 16; - TRY_GET_INT_ATTRIBUTE(jsonObj, iconExtent); - iconSize = QSize{ iconExtent, iconExtent }; - iconSizeMedium = iconSize * 1.5; - iconSizeLarge = iconSize * 2; - iconSizeExtraSmall = iconSize * 0.75; - - TRY_GET_INT_ATTRIBUTE(jsonObj, sliderTickSize); - TRY_GET_INT_ATTRIBUTE(jsonObj, sliderTickSpacing); - TRY_GET_INT_ATTRIBUTE(jsonObj, sliderTickThickness); - TRY_GET_INT_ATTRIBUTE(jsonObj, sliderGrooveHeight); - TRY_GET_INT_ATTRIBUTE(jsonObj, progressBarGrooveHeight); - TRY_GET_INT_ATTRIBUTE(jsonObj, spacing); - TRY_GET_INT_ATTRIBUTE(jsonObj, scrollBarThicknessFull); - TRY_GET_INT_ATTRIBUTE(jsonObj, scrollBarThicknessSmall); - TRY_GET_INT_ATTRIBUTE(jsonObj, scrollBarMargin); - TRY_GET_INT_ATTRIBUTE(jsonObj, tabBarPaddingTop); - TRY_GET_INT_ATTRIBUTE(jsonObj, tabBarTabMaxWidth); - TRY_GET_INT_ATTRIBUTE(jsonObj, tabBarTabMinWidth); - - tabBarTabMaxWidth = std::max(0, tabBarTabMaxWidth); - tabBarTabMinWidth = std::max(0, tabBarTabMinWidth); - if (tabBarTabMinWidth > tabBarTabMaxWidth) { - std::swap(tabBarTabMinWidth, tabBarTabMaxWidth); - } +bool Theme::initializeFromJson(QJsonDocument const& jsonDoc) { + if (!jsonDoc.isObject()) + return false; + + const auto jsonObj = jsonDoc.object(); + if (!jsonObj.isEmpty()) { + // Parse metadata. + auto const metaObj = jsonObj.value(QStringLiteral("meta")).toObject(); + if (!jsonObjHasAllKeys(metaObj, { + QStringLiteral("name"), + QStringLiteral("version"), + QStringLiteral("author"), + })) + return false; + + meta = ThemeMeta{ + tryGetString(metaObj, QStringLiteral("name"), {}), + tryGetString(metaObj, QStringLiteral("version"), {}), + tryGetString(metaObj, QStringLiteral("author"), {}), + }; + + // Parse all values. + TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorMain1); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorMain2); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorMain3); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorMain4); + backgroundColorMainTransparent = colorWithAlpha(backgroundColorMain1, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorWorkspace); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, backgroundColorTabBar); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, neutralColorDisabled); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, neutralColor); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, neutralColorHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, neutralColorPressed); + neutralColorTransparent = colorWithAlpha(neutralColorDisabled, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, focusColor); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColor); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorDisabled); + primaryColorTransparent = colorWithAlpha(primaryColor, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorForeground); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorForegroundHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorForegroundPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryColorForegroundDisabled); + primaryColorForegroundTransparent = colorWithAlpha(primaryColorForeground, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryAlternativeColor); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryAlternativeColorHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryAlternativeColorPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, primaryAlternativeColorDisabled); + primaryAlternativeColorTransparent = colorWithAlpha(primaryAlternativeColor, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColor); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorDisabled); + secondaryColorTransparent = colorWithAlpha(secondaryColor, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryAlternativeColor); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryAlternativeColorHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryAlternativeColorPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryAlternativeColorDisabled); + secondaryAlternativeColorTransparent = colorWithAlpha(secondaryAlternativeColor, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorForeground); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorForegroundHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorForegroundPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, secondaryColorForegroundDisabled); + secondaryColorForegroundTransparent = colorWithAlpha(secondaryColorForeground, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, semiTransparentColor1); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, semiTransparentColor2); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, semiTransparentColor3); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, semiTransparentColor4); + semiTransparentColorTransparent = colorWithAlpha(semiTransparentColor1, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorSuccess); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorSuccessHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorSuccessPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorSuccessDisabled); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorInfo); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorInfoHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorInfoPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorInfoDisabled); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorWarning); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorWarningHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorWarningPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorWarningDisabled); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorError); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorErrorHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorErrorPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorErrorDisabled); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorForeground); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorForegroundHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorForegroundPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, statusColorForegroundDisabled); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, borderColor); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, borderColorHovered); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, borderColorPressed); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, borderColorDisabled); + borderColorTransparent = colorWithAlpha(borderColor, 0); + + TRY_GET_COLOR_ATTRIBUTE(jsonObj, shadowColor1); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, shadowColor2); + TRY_GET_COLOR_ATTRIBUTE(jsonObj, shadowColor3); + shadowColorTransparent = colorWithAlpha(shadowColor1, 0); + + TRY_GET_INT_ATTRIBUTE(jsonObj, fontSize); + TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH1); + TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH2); + TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH3); + TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH4); + TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeH5); + TRY_GET_INT_ATTRIBUTE(jsonObj, fontSizeS1); + TRY_GET_INT_ATTRIBUTE(jsonObj, animationDuration); + TRY_GET_INT_ATTRIBUTE(jsonObj, focusAnimationDuration); + TRY_GET_INT_ATTRIBUTE(jsonObj, sliderAnimationDuration); + TRY_GET_DOUBLE_ATTRIBUTE(jsonObj, borderRadius); + TRY_GET_DOUBLE_ATTRIBUTE(jsonObj, checkBoxBorderRadius); + TRY_GET_DOUBLE_ATTRIBUTE(jsonObj, menuItemBorderRadius); + TRY_GET_DOUBLE_ATTRIBUTE(jsonObj, menuBarItemBorderRadius); + TRY_GET_INT_ATTRIBUTE(jsonObj, borderWidth); + TRY_GET_INT_ATTRIBUTE(jsonObj, controlHeightLarge); + TRY_GET_INT_ATTRIBUTE(jsonObj, controlHeightMedium); + TRY_GET_INT_ATTRIBUTE(jsonObj, controlHeightSmall); + TRY_GET_INT_ATTRIBUTE(jsonObj, controlDefaultWidth); + TRY_GET_INT_ATTRIBUTE(jsonObj, dialMarkLength); + TRY_GET_INT_ATTRIBUTE(jsonObj, dialMarkThickness); + TRY_GET_INT_ATTRIBUTE(jsonObj, dialTickLength); + TRY_GET_INT_ATTRIBUTE(jsonObj, dialTickSpacing); + TRY_GET_INT_ATTRIBUTE(jsonObj, dialGrooveThickness); + TRY_GET_INT_ATTRIBUTE(jsonObj, focusBorderWidth); + + auto iconExtent = 16; + TRY_GET_INT_ATTRIBUTE(jsonObj, iconExtent); + iconSize = QSize{ iconExtent, iconExtent }; + iconSizeMedium = iconSize * 1.5; + iconSizeLarge = iconSize * 2; + iconSizeExtraSmall = iconSize * 0.75; + + TRY_GET_INT_ATTRIBUTE(jsonObj, sliderTickSize); + TRY_GET_INT_ATTRIBUTE(jsonObj, sliderTickSpacing); + TRY_GET_INT_ATTRIBUTE(jsonObj, sliderTickThickness); + TRY_GET_INT_ATTRIBUTE(jsonObj, sliderGrooveHeight); + TRY_GET_INT_ATTRIBUTE(jsonObj, progressBarGrooveHeight); + TRY_GET_INT_ATTRIBUTE(jsonObj, spacing); + TRY_GET_INT_ATTRIBUTE(jsonObj, scrollBarThicknessFull); + TRY_GET_INT_ATTRIBUTE(jsonObj, scrollBarThicknessSmall); + TRY_GET_INT_ATTRIBUTE(jsonObj, scrollBarMargin); + TRY_GET_INT_ATTRIBUTE(jsonObj, tabBarPaddingTop); + TRY_GET_INT_ATTRIBUTE(jsonObj, tabBarTabMaxWidth); + TRY_GET_INT_ATTRIBUTE(jsonObj, tabBarTabMinWidth); + + tabBarTabMaxWidth = std::max(0, tabBarTabMaxWidth); + tabBarTabMinWidth = std::max(0, tabBarTabMinWidth); + if (tabBarTabMinWidth > tabBarTabMaxWidth) { + std::swap(tabBarTabMinWidth, tabBarTabMaxWidth); } } + + return true; } QJsonDocument Theme::toJson() const { @@ -423,6 +455,9 @@ QJsonDocument Theme::toJson() const { SET_COLOR(jsonObj, backgroundColorMain3); SET_COLOR(jsonObj, backgroundColorMain4); + SET_COLOR(jsonObj, backgroundColorWorkspace); + SET_COLOR(jsonObj, backgroundColorTabBar); + SET_COLOR(jsonObj, neutralColor); SET_COLOR(jsonObj, neutralColorHovered); SET_COLOR(jsonObj, neutralColorPressed); @@ -551,6 +586,8 @@ bool Theme::operator==(const Theme& other) const { && backgroundColorMain3 == other.backgroundColorMain3 && backgroundColorMain4 == other.backgroundColorMain4 + && backgroundColorWorkspace == other.backgroundColorWorkspace + && neutralColorDisabled == other.neutralColorDisabled && neutralColor == other.neutralColor && neutralColorHovered == other.neutralColorHovered diff --git a/lib/src/style/ThemeManager.cpp b/lib/src/style/ThemeManager.cpp index 93cb279..4e2c12b 100644 --- a/lib/src/style/ThemeManager.cpp +++ b/lib/src/style/ThemeManager.cpp @@ -3,6 +3,8 @@ #include +#include + namespace oclero::qlementine { ThemeManager::ThemeManager(QObject* parent) : ThemeManager(nullptr, parent) {} @@ -10,6 +12,9 @@ ThemeManager::ThemeManager(QObject* parent) ThemeManager::ThemeManager(QlementineStyle* style, QObject* parent) : QObject(parent) { setStyle(style); + if (parent == nullptr) { + setParent(style); + } } QlementineStyle* ThemeManager::style() const { @@ -32,11 +37,30 @@ const std::vector& ThemeManager::themes() const { void ThemeManager::addTheme(const Theme& theme) { _themes.emplace_back(theme); emit themeCountChanged(); - if (_currentIndex == -1) { + if (_currentIndex < 0) { setCurrentThemeIndex(0); } } +void ThemeManager::loadDirectory(const QString& path) { + QDir dir(path); + if (!dir.exists()) + return; + + dir.setFilter(QDir::Filter::Files | QDir::Filter::NoDotAndDotDot); + dir.setSorting(QDir::SortFlag::Name | QDir::SortFlag::IgnoreCase); + const auto files = dir.entryInfoList(); + for (const auto& file : files) { + QFileInfo fileInfo(file); + if (fileInfo.suffix().toLower() == QStringLiteral("json")) { + const auto themeOpt = Theme::fromJsonPath(file.absoluteFilePath()); + if (themeOpt.has_value()) { + addTheme(themeOpt.value()); + } + } + } +} + QString ThemeManager::currentTheme() const { if (_currentIndex > -1 && _currentIndex < themeCount()) { return _themes.at(_currentIndex).meta.name; @@ -54,11 +78,12 @@ int ThemeManager::currentThemeIndex() const { } void ThemeManager::setCurrentThemeIndex(int index) { - index = std::max(-1, std::min(themeCount() - 1, index)); - if (index != _currentIndex) { - _currentIndex = index; - synchronizeThemeOnStyle(); - emit currentThemeChanged(); + if (index > -1 && index < themeCount()) { + if (index != _currentIndex) { + _currentIndex = index; + synchronizeThemeOnStyle(); + emit currentThemeChanged(); + } } } @@ -93,18 +118,18 @@ int ThemeManager::themeIndex(const QString& key) const { return -1; } -QString ThemeManager::getLocalizedThemeName(const QString& baseThemeName) const { - if (baseThemeName.toLower() == QStringLiteral("light")) { - return tr("Light Theme"); - } else if (baseThemeName.toLower() == QStringLiteral("dark")) { - return tr("Dark Theme"); - } - return baseThemeName; -} - void ThemeManager::synchronizeThemeOnStyle() { - if (_style && _currentIndex != -1 && !_themes.empty() && _currentIndex < themeCount()) { + if (!_style) + return; + + if (_themes.empty()) + return; + + if (_currentIndex >= 0 && _currentIndex < themeCount()) { _style->setTheme(_themes.at(_currentIndex)); + } else { + addTheme(_style->theme()); + setCurrentThemeIndex(themeCount() - 1); } } } // namespace oclero::qlementine diff --git a/lib/src/tools/ThemeEditor.cpp b/lib/src/tools/ThemeEditor.cpp index 130f881..d727346 100644 --- a/lib/src/tools/ThemeEditor.cpp +++ b/lib/src/tools/ThemeEditor.cpp @@ -220,8 +220,12 @@ struct ThemeEditor::Impl { // Get theme from file and set it on the application. const auto fileName = QFileDialog::getOpenFileName(&owner, "Load JSON theme", previousPath, "JSON Files (*.json)"); - const auto theme = Theme(fileName); - owner.setTheme(theme); + const auto themeOpt = Theme::fromJsonPath(fileName); + if (!themeOpt.has_value()) { + return; + } + + owner.setTheme(themeOpt.value()); // Save path to QSettings. settings.setValue(PREVIOUS_PATH_SETTINGS_KEY, fileName); diff --git a/lib/src/utils/IconUtils.cpp b/lib/src/utils/IconUtils.cpp new file mode 100644 index 0000000..cc33a6d --- /dev/null +++ b/lib/src/utils/IconUtils.cpp @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Olivier Cléro +// SPDX-License-Identifier: MIT + +#include + +#include + +#include +#include + +namespace oclero::qlementine { +IconTheme::IconTheme(const QColor& normal, const QColor& disabled, const QColor& checkedNormal, QColor checkedDisabled) + : normal(normal) + , disabled(disabled) + , checkedNormal(checkedNormal) + , checkedDisabled(checkedDisabled) {} + +IconTheme::IconTheme(const QColor& normal, const QColor& disabled) + : IconTheme(normal, disabled, normal, disabled) {} + +IconTheme::IconTheme(const QColor& normal) + : IconTheme(normal, normal, normal, normal) {} + +const QColor& IconTheme::color(QIcon::Mode mode, QIcon::State state) const { + switch (mode) { + case QIcon::Disabled: + return state == QIcon::On ? checkedDisabled : disabled; + case QIcon::Normal: + case QIcon::Active: + case QIcon::Selected: + default: + return state == QIcon::On ? checkedNormal : normal; + } +} + +QIcon makeIconFromSvg(const QString& svgPath, const QSize& size) { + if (svgPath.isEmpty() || size.isEmpty()) + return {}; + + QIcon icon; + + QSvgRenderer svgRenderer(svgPath); + svgRenderer.setAspectRatioMode(Qt::AspectRatioMode::KeepAspectRatio); + + for (const auto pxRatio : { 1., 2. }) { + QPixmap pixmap(size * pxRatio); + pixmap.fill(Qt::transparent); + { + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + svgRenderer.render(&painter, pixmap.rect()); + } + pixmap.setDevicePixelRatio(pxRatio); + + for (const auto iconMode : { QIcon::Normal, QIcon::Disabled, QIcon::Active, QIcon::Selected }) { + for (const auto iconState : { QIcon::On, QIcon::Off }) { + icon.addPixmap(pixmap, iconMode, iconState); + } + } + } + + return icon; +} + +QIcon makeIconFromSvg(const QString& svgPath, const IconTheme& iconTheme, const QSize& size) { + if (svgPath.isEmpty() || size.isEmpty()) + return {}; + + QIcon icon; + + QSvgRenderer svgRenderer(svgPath); + svgRenderer.setAspectRatioMode(Qt::AspectRatioMode::KeepAspectRatio); + + for (const auto pxRatio : { 1, 2 }) { + QPixmap pixmap(size * pxRatio); + pixmap.fill(Qt::transparent); + { + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + svgRenderer.render(&painter, pixmap.rect()); + } + pixmap.setDevicePixelRatio(static_cast(pxRatio)); + + for (const auto iconMode : { QIcon::Normal, QIcon::Disabled, QIcon::Active, QIcon::Selected }) { + for (const auto iconState : { QIcon::On, QIcon::Off }) { + const auto& fgColor = iconTheme.color(iconMode, iconState); + const auto coloredPixmap = qlementine::getColorizedPixmap(pixmap, fgColor); + icon.addPixmap(coloredPixmap, iconMode, iconState); + } + } + } + + return icon; +} +} // namespace oclero::qlementine diff --git a/lib/src/utils/ImageUtils.cpp b/lib/src/utils/ImageUtils.cpp index c351e84..f2ee77e 100644 --- a/lib/src/utils/ImageUtils.cpp +++ b/lib/src/utils/ImageUtils.cpp @@ -165,26 +165,6 @@ QPixmap getCachedPixmap(QPixmap const& input, QColor const& color, ColorizeMode return pixmapInCache.isNull() ? input : pixmapInCache; } -QIcon makeIconFromSvg(const QString& svgPath, const QSize& size) { - if (svgPath.isEmpty()) - return {}; - - QIcon icon; - QSvgRenderer renderer(svgPath); - constexpr auto ratios = std::array{ 1, 2 }; - for (const auto& ratio : ratios) { - const auto pixmapSize = size * ratio; - QPixmap pixmap(pixmapSize); - pixmap.fill(Qt::transparent); - QPainter painter(&pixmap); - painter.setRenderHint(QPainter::Antialiasing, true); - renderer.render(&painter, pixmap.rect()); - pixmap.setDevicePixelRatio(static_cast(ratio)); - icon.addPixmap(pixmap); - } - return icon; -} - QPixmap makePixmapFromSvg(const QString& svgPath, const QSize& size) { if (svgPath.isEmpty()) return {}; diff --git a/lib/src/utils/LayoutUtils.cpp b/lib/src/utils/LayoutUtils.cpp new file mode 100644 index 0000000..ee1274d --- /dev/null +++ b/lib/src/utils/LayoutUtils.cpp @@ -0,0 +1,56 @@ +#include + +#include +#include + +namespace oclero { +namespace qlementine { +QMargins getLayoutMargins(const QWidget* widget) { + if (const auto* style = widget ? widget->style() : nullptr) { + const auto left = style->pixelMetric(QStyle::PM_LayoutLeftMargin); + const auto top = style->pixelMetric(QStyle::PM_LayoutTopMargin); + const auto right = style->pixelMetric(QStyle::PM_LayoutRightMargin); + const auto bottom = style->pixelMetric(QStyle::PM_LayoutBottomMargin); + return { left, top, right, bottom }; + } + return { 0, 0, 0, 0 }; +} + +int getLayoutHSpacing(const QWidget* widget) { + if (const auto* style = widget ? widget->style() : nullptr) { + return style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); + } + return 0; +} + +int getLayoutVSpacing(const QWidget* widget) { + if (const auto* style = widget ? widget->style() : nullptr) { + return style->pixelMetric(QStyle::PM_LayoutVerticalSpacing); + } + return 0; +} + +std::tuple getVLayoutProps(const QWidget* widget) { + return std::tuple{ getLayoutVSpacing(widget), getLayoutMargins(widget) }; +} + +std::tuple getHLayoutProps(const QWidget* widget) { + return std::tuple{ getLayoutHSpacing(widget), getLayoutMargins(widget) }; +} + +std::tuple getFormLayoutProps(const QWidget* widget) { + return std::tuple{ getLayoutVSpacing(widget), getLayoutHSpacing(widget), getLayoutMargins(widget) }; +} + +void clearLayout(QLayout* layout) { + while (auto* item = layout->takeAt(0)) { + if (auto* widget = item->widget()) { + delete widget; + } else if (auto* item_layout = item->layout()) { + clearLayout(item_layout); + } + delete item; + } +} +} // namespace qlementine +} // namespace oclero diff --git a/lib/src/utils/MenuUtils.cpp b/lib/src/utils/MenuUtils.cpp new file mode 100644 index 0000000..93dbb10 --- /dev/null +++ b/lib/src/utils/MenuUtils.cpp @@ -0,0 +1,69 @@ +#include + +#include +#include +#include +#include + +namespace oclero::qlementine { +class FlashActionHelper : QObject { +public: + FlashActionHelper(QAction* action, QMenu* menu, const std::function& onAnimationFinished) + : QObject(action) + , _menu(menu) + , _action(action) + , _onAnimationFinished(onAnimationFinished) { + if (_menu && _action) { + _action->setProperty("qlementine_flashing", true); + _menu->blockSignals(true); + _timerId = startTimer(flashActionBlinkDuration); + } + } + +protected: + void timerEvent(QTimerEvent*) override { + if (_flashActionElapsedTime < flashActionDuration && _menu && _action) { + _flashActionElapsedTime += flashActionBlinkDuration; + const auto* currentActiveAction = _menu->activeAction(); + _menu->setActiveAction(currentActiveAction == nullptr ? _action : nullptr); + } else { + killTimer(_timerId); + if (_menu) { + if (_action) { + _menu->setActiveAction(_action); + _action->setProperty("qlementine_flashing", false); + } + _menu->blockSignals(false); + } + if (_onAnimationFinished) { + _onAnimationFinished(); + } + } + } + +private: + static constexpr int flashActionBlinkDuration{ 60 }; // ms + static constexpr int flashActionDuration{ 2 * flashActionBlinkDuration }; // ms + int _flashActionElapsedTime{ 0 }; // ms + int _timerId{ -1 }; + QPointer _menu{ nullptr }; + QPointer _action{ nullptr }; + std::function _onAnimationFinished{}; +}; + +QMenu* getTopLevelMenu(QMenu* menu) { + auto parent = menu; + while (parent != nullptr) { + auto parent_menu = qobject_cast(parent->parentWidget()); + if (parent_menu != nullptr) + parent = parent_menu; + else + break; + } + return parent; +} + +void flashAction(QAction* action, QMenu* menu, const std::function& onAnimationFinished) { + new FlashActionHelper(action, menu, onAnimationFinished); +} +} // namespace oclero::qlementine diff --git a/lib/src/utils/WidgetUtils.cpp b/lib/src/utils/WidgetUtils.cpp index d92f4fc..01bfa7d 100644 --- a/lib/src/utils/WidgetUtils.cpp +++ b/lib/src/utils/WidgetUtils.cpp @@ -61,18 +61,6 @@ void centerWidget(QWidget* widget, QWidget* host) { } } -QMargins getDefaultMargins(const QStyle* style) { - if (!style) - return { 0, 0, 0, 0 }; - - const auto paddingLeft = style->pixelMetric(QStyle::PM_LayoutLeftMargin); - const auto paddingRight = style->pixelMetric(QStyle::PM_LayoutRightMargin); - const auto paddingTop = style->pixelMetric(QStyle::PM_LayoutTopMargin); - const auto paddingBottom = style->pixelMetric(QStyle::PM_LayoutBottomMargin); - const auto contentsMargins = QMargins{ paddingLeft, paddingTop, paddingRight, paddingBottom }; - return contentsMargins; -} - qreal getDpi(const QWidget* widget) { if (widget) { if (const auto* screen = widget->screen()) { @@ -90,21 +78,4 @@ QWindow* getWindow(const QWidget* widget) { } return nullptr; } - -void clearLayout(QLayout* layout) { - if (!layout) - return; - - QLayoutItem* item{}; - while ((item = layout->takeAt(0))) { - if (item->layout()) { - clearLayout(item->layout()); - delete item->layout(); - } - if (item->widget()) { - delete item->widget(); - } - delete item; - } -} } // namespace oclero::qlementine diff --git a/lib/src/widgets/Expander.cpp b/lib/src/widgets/Expander.cpp index f91a397..db6882b 100644 --- a/lib/src/widgets/Expander.cpp +++ b/lib/src/widgets/Expander.cpp @@ -7,13 +7,20 @@ #include namespace oclero::qlementine { -constexpr auto animationDurationFactor = 1; +constexpr auto animationDurationFactor = 1.; Expander::Expander(QWidget* parent) : QWidget(parent) { - const auto animationDuration = style()->styleHint(QStyle::SH_Widget_Animation_Duration); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); setFocusPolicy(Qt::NoFocus); + + if (_orientation == Qt::Vertical) { + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + } else { + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + } + + // Initialize animation. + const auto animationDuration = style()->styleHint(QStyle::SH_Widget_Animation_Duration); _animation.setStartValue(QVariant::fromValue(0)); _animation.setEndValue(QVariant::fromValue(0)); _animation.setDuration(animationDuration * animationDurationFactor); @@ -21,14 +28,32 @@ Expander::Expander(QWidget* parent) QObject::connect(&_animation, &QVariantAnimation::valueChanged, this, [this]() { updateGeometry(); }); + + QObject::connect(&_animation, &QVariantAnimation::finished, this, [this]() { + if (_expanded) { + emit didExpand(); + } else { + emit didShrink(); + } + }); } QSize Expander::sizeHint() const { const auto contentSizeHint = _content ? _content->sizeHint() : QSize{ 0, 0 }; - const auto w = contentSizeHint.width(); - const auto h = _animation.state() == QVariantAnimation::Running ? _animation.currentValue().toInt() - : (_expanded ? contentSizeHint.height() : 0); - return { w, h }; + const auto isVertical = _orientation == Qt::Orientation::Vertical; + const auto currentValue = _animation.currentValue().toInt(); + + if (isVertical) { + const auto finalValue = _expanded ? contentSizeHint.height() : 0; + const auto w = contentSizeHint.width(); + const auto h = _animation.state() == QVariantAnimation::Running ? currentValue : finalValue; + return { w, h }; + } else { + const auto finalValue = _expanded ? contentSizeHint.width() : 0; + const auto h = contentSizeHint.height(); + const auto w = _animation.state() == QVariantAnimation::Running ? currentValue : finalValue; + return { w, h }; + } } bool Expander::event(QEvent* e) { @@ -58,9 +83,11 @@ void Expander::updateContentGeometry() { _content->ensurePolished(); const auto& availableSize = size(); const auto contentSizeHint = _content->sizeHint(); - const auto w = availableSize.width(); - const auto h = contentSizeHint.height(); - _content->setVisible(w > 0); + const auto isVertical = _orientation == Qt::Orientation::Vertical; + const auto w = isVertical ? availableSize.width() : contentSizeHint.width(); + const auto h = isVertical ? contentSizeHint.height() : availableSize.height(); + const auto visible = isVertical ? w > 0 : h > 0; + _content->setVisible(visible); _content->setGeometry(0, 0, w, h); } } @@ -76,8 +103,16 @@ void Expander::setExpanded(bool expanded) { } _expanded = expanded; - const auto current = height(); - const auto target = _content && _expanded ? _content->sizeHint().height() : 0; + if (_expanded) { + emit aboutToExpand(); + } else { + emit aboutToShrink(); + } + + const auto isVertical = _orientation == Qt::Orientation::Vertical; + const auto current = isVertical ? height() : width(); + const auto contentSizeHint = _content->sizeHint(); + const auto target = _content && _expanded ? (isVertical ? contentSizeHint.height() : contentSizeHint.width()) : 0; const auto animationDuration = isVisible() ? style()->styleHint(QStyle::SH_Widget_Animation_Duration) : 0; _animation.stop(); _animation.setDuration(animationDuration * animationDurationFactor); @@ -89,6 +124,29 @@ void Expander::setExpanded(bool expanded) { } } +void Expander::toggleExpanded() { + setExpanded(!expanded()); +} + +Qt::Orientation Expander::orientation() const { + return _orientation; +} + +void Expander::setOrientation(Qt::Orientation orientation) { + if (_orientation != orientation) { + _orientation = orientation; + + if (_orientation == Qt::Vertical) { + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + } else { + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + } + + updateGeometry(); + emit orientationChanged(); + } +} + QWidget* Expander::content() const { return _content; } @@ -102,7 +160,12 @@ void Expander::setContent(QWidget* content) { _content = content; if (_content) { - _content->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Ignored); + if (_orientation == Qt::Orientation::Vertical) { + _content->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Ignored); + } else { + _content->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::MinimumExpanding); + } + _content->setParent(this); _content->installEventFilter(this); _content->setVisible(_expanded); diff --git a/lib/src/widgets/NavigationBar.cpp b/lib/src/widgets/NavigationBar.cpp index 868ed3b..51cf411 100644 --- a/lib/src/widgets/NavigationBar.cpp +++ b/lib/src/widgets/NavigationBar.cpp @@ -16,11 +16,11 @@ const QColor& NavigationBar::getBgColor(const Theme& theme) const { const QColor& NavigationBar::getItemBgColor(MouseState mouse, const Theme& theme) const { switch (mouse) { case MouseState::Hovered: - return theme.neutralColorHovered; + return theme.backgroundColorMain3; case MouseState::Pressed: - return theme.neutralColorPressed; + return theme.backgroundColorMain4; default: - return theme.neutralColorTransparent; + return theme.backgroundColorMainTransparent; } } diff --git a/lib/src/widgets/SegmentedControl.cpp b/lib/src/widgets/SegmentedControl.cpp index 0ff3272..049f706 100644 --- a/lib/src/widgets/SegmentedControl.cpp +++ b/lib/src/widgets/SegmentedControl.cpp @@ -15,11 +15,11 @@ const QColor& SegmentedControl::getBgColor(const Theme& theme) const { const QColor& SegmentedControl::getItemBgColor(MouseState mouse, const Theme& theme) const { switch (mouse) { case MouseState::Hovered: - return theme.semiTransparentColor2; + return theme.neutralColor; case MouseState::Pressed: - return theme.semiTransparentColor4; + return theme.neutralColorHovered; default: - return theme.semiTransparentColorTransparent; + return theme.neutralColorTransparent; } } diff --git a/lib/src/widgets/Switch.cpp b/lib/src/widgets/Switch.cpp index 946ee07..107f098 100644 --- a/lib/src/widgets/Switch.cpp +++ b/lib/src/widgets/Switch.cpp @@ -122,7 +122,9 @@ void Switch::enterEvent(QEnterEvent* e) { void Switch::changeEvent(QEvent* e) { QAbstractButton::changeEvent(e); - if (e->type() == QEvent::Type::EnabledChange) { + const auto type = e->type(); + if (type == QEvent::Type::EnabledChange || type == QEvent::Type::PaletteChange + || type == QEvent::Type::ApplicationPaletteChange) { startAnimation(); } } diff --git a/sandbox/CMakeLists.txt b/sandbox/CMakeLists.txt index f5ded43..33b28a6 100644 --- a/sandbox/CMakeLists.txt +++ b/sandbox/CMakeLists.txt @@ -2,8 +2,9 @@ set(SANDBOX_NAME "sandbox") if(APPLE) set(APP_ICON_MACOS "${CMAKE_SOURCE_DIR}/branding/icon/icon.icns") - set_source_files_properties(${APP_ICON_MACOS} PROPERTIES - MACOSX_PACKAGE_LOCATION "Resources" + set_source_files_properties(${APP_ICON_MACOS} + PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" ) endif() @@ -19,53 +20,29 @@ target_link_libraries(${SANDBOX_NAME} PUBLIC oclero::qlementine ) -install(TARGETS ${SANDBOX_NAME} - BUNDLE DESTINATION . +install( + TARGETS ${SANDBOX_NAME} + BUNDLE DESTINATION . RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) -set_target_properties(${SANDBOX_NAME} PROPERTIES - INTERNAL_CONSOLE OFF - EXCLUDE_FROM_ALL OFF - FOLDER "sandbox" - CMAKE_AUTOMOC ON - CMAKE_AUTORCC ON - CMAKE_AUTOUIC ON - - MACOSX_BUNDLE_GUI_IDENTIFIER "oclero.qlementine.${SANDBOX_NAME}" - MACOSX_BUNDLE_BUNDLE_NAME "Sandbox" - MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} - MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} - MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} - MACOSX_BUNDLE_ICON_FILE "icon.icns" - MACOSX_BUNDLE_COPYRIGHT ${PROJECT_COPYRIGHT} - - XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED OFF - XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" - XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual" - XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS OFF +set_target_properties(${SANDBOX_NAME} + PROPERTIES + INTERNAL_CONSOLE OFF + EXCLUDE_FROM_ALL OFF + FOLDER "tools" + CMAKE_AUTOMOC ON + CMAKE_AUTORCC ON + CMAKE_AUTOUIC ON + MACOSX_BUNDLE_GUI_IDENTIFIER "oclero.qlementine.${SANDBOX_NAME}" + MACOSX_BUNDLE_BUNDLE_NAME "Sandbox" + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_ICON_FILE "icon.icns" + MACOSX_BUNDLE_COPYRIGHT ${PROJECT_COPYRIGHT} + XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "${XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED}" + XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY}" + XCODE_ATTRIBUTE_CODE_SIGN_STYLE "${XCODE_ATTRIBUTE_CODE_SIGN_STYLE}" + XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS OFF ) - -# target_deploy_qt(sandbox) - -# if(${QT_VERSION_MAJOR} STREQUAL "6") -# # NB: Broken in 6.7.x - -# # qt_generate_deploy_app_script( -# # TARGET ${SANDBOX_NAME} -# # OUTPUT_SCRIPT SANDBOX_DEPLOY_SCRIPT -# # ) - -# # add_custom_command(TARGET ${SANDBOX_NAME} POST_BUILD -# # COMMENT "Deploying Qt..." -# # COMMAND ${CMAKE_COMMAND} -P ${SANDBOX_DEPLOY_SCRIPT} -DQT_DEPLOY_PREFIX=$ -DQT_DEPLOY_BIN_DIR=. -# # ) -# # install(SCRIPT ${SANDBOX_DEPLOY_SCRIPT}) - -# # add_custom_command(TARGET ${SANDBOX_NAME} POST_BUILD -# # COMMENT "Deploying Qt..." -# # COMMAND ${CMAKE_COMMAND} -P ${SANDBOX_DEPLOY_SCRIPT} -# # ) -# else() -# target_deploy_qt(sandbox) -# endif() diff --git a/sandbox/resources/dark.json b/sandbox/resources/dark.json deleted file mode 100644 index 663b1bc..0000000 --- a/sandbox/resources/dark.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "backgroundColorMain1": "#1f1f1f", - "backgroundColorMain2": "#2a2a2a", - "backgroundColorMain3": "#363636", - "backgroundColorMain4": "#414141", - "borderColor": "#4b4b4b", - "borderColorDisabled": "#444444", - "borderColorHovered": "#717171", - "borderColorPressed": "#a0a0a0", - "focusColor": "#34988666", - "meta": { - "author": "Olivier Cléro", - "name": "Dark", - "version": "1.4.0" - }, - "neutralColor": "#565656", - "neutralColorDisabled": "#353535", - "neutralColorHovered": "#4c4c4c", - "neutralColorPressed": "#404040", - "primaryAlternativeColor": "#126d5d", - "primaryAlternativeColorDisabled": "#1C3B36", - "primaryAlternativeColorHovered": "#0d6354", - "primaryAlternativeColorPressed": "#177665", - "primaryColor": "#349886", - "primaryColorDisabled": "#2c403c", - "primaryColorForeground": "#ffffff", - "primaryColorForegroundDisabled": "#3a534d", - "primaryColorForegroundHovered": "#ffffff", - "primaryColorForegroundPressed": "#ffffff", - "primaryColorHovered": "#2a7a6b", - "primaryColorPressed": "#2f8979", - "secondaryAlternativeColor": "#8f8f8f", - "secondaryAlternativeColorDisabled": "#8f8f8f3f", - "secondaryAlternativeColorHovered": "#797979", - "secondaryAlternativeColorPressed": "#848484", - "secondaryColor": "#ffffff", - "secondaryColorDisabled": "#ffffff33", - "secondaryColorForeground": "#2f2f2f", - "secondaryColorForegroundDisabled": "#2f2f2f", - "secondaryColorForegroundHovered": "#2f2f2f", - "secondaryColorForegroundPressed": "#2f2f2f", - "secondaryColorHovered": "#d5d5d5", - "secondaryColorPressed": "#ebebeb", - "semiTransparentColor1": "0xffffff18", - "semiTransparentColor2": "0xffffff23", - "semiTransparentColor3": "0xffffff28", - "semiTransparentColor4": "0xffffff2d", - "shadowColor1": "#00000066", - "shadowColor2": "#000000bb", - "shadowColor3": "#000000ff", - "statusColorError": "#ef5151", - "statusColorErrorDisabled": "#ef515133", - "statusColorErrorHovered": "#c64848", - "statusColorErrorPressed": "#da4d4d", - "statusColorForeground": "#ffffff", - "statusColorForegroundDisabled": "#ffffff26", - "statusColorForegroundHovered": "#ffffff", - "statusColorForegroundPressed": "#ffffff", - "statusColorInfo": "#4ab9e9", - "statusColorInfoDisabled": "#4ab9e933", - "statusColorInfoHovered": "#429bc1", - "statusColorInfoPressed": "#46aad6", - "statusColorSuccess": "#32cd79", - "statusColorSuccessDisabled": "#32cd7a26", - "statusColorSuccessHovered": "#2eaa68", - "statusColorSuccessPressed": "#30bc71", - "statusColorWarning": "#ffcd1e", - "statusColorWarningDisabled": "#ffcd1e33", - "statusColorWarningHovered": "#d2ab1f", - "statusColorWarningPressed": "#e9bc1f" -} diff --git a/sandbox/resources/plus_24.svg b/sandbox/resources/plus_24.svg deleted file mode 100644 index 1bcc698..0000000 --- a/sandbox/resources/plus_24.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/sandbox/resources/refresh.svg b/sandbox/resources/refresh.svg deleted file mode 100644 index 2f29c6b..0000000 --- a/sandbox/resources/refresh.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/sandbox/resources/sandbox.qrc b/sandbox/resources/sandbox.qrc index 3d148a6..df575d2 100644 --- a/sandbox/resources/sandbox.qrc +++ b/sandbox/resources/sandbox.qrc @@ -1,14 +1,12 @@ - - dark.json - light.json + + themes/dark.json + themes/light.json qlementine_icon.ico qlementine_icon.icns - refresh.svg - plus_24.svg test_image_16x16.png - scene_light.svg - scene_material.svg - scene_object.svg + test_image_16x16.svg + test_image_24x24.svg + test_image_color_16x16.svg diff --git a/sandbox/resources/scene_light.svg b/sandbox/resources/scene_light.svg deleted file mode 100644 index 5e46abf..0000000 --- a/sandbox/resources/scene_light.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/sandbox/resources/scene_material.svg b/sandbox/resources/scene_material.svg deleted file mode 100644 index e46b631..0000000 --- a/sandbox/resources/scene_material.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/sandbox/resources/scene_object.svg b/sandbox/resources/scene_object.svg deleted file mode 100644 index 57bf3b9..0000000 --- a/sandbox/resources/scene_object.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/sandbox/resources/test_image_16x16.svg b/sandbox/resources/test_image_16x16.svg new file mode 100644 index 0000000..b4b1bb2 --- /dev/null +++ b/sandbox/resources/test_image_16x16.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sandbox/resources/test_image_24x24.svg b/sandbox/resources/test_image_24x24.svg new file mode 100644 index 0000000..e306dad --- /dev/null +++ b/sandbox/resources/test_image_24x24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sandbox/resources/test_image_color_16x16.svg b/sandbox/resources/test_image_color_16x16.svg new file mode 100644 index 0000000..c8e978e --- /dev/null +++ b/sandbox/resources/test_image_color_16x16.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sandbox/resources/themes b/sandbox/resources/themes new file mode 120000 index 0000000..afbec12 --- /dev/null +++ b/sandbox/resources/themes @@ -0,0 +1 @@ +../../showcase/resources/themes \ No newline at end of file diff --git a/sandbox/src/CsdWindow.cpp b/sandbox/src/CsdWindow.cpp deleted file mode 100644 index f395bc8..0000000 --- a/sandbox/src/CsdWindow.cpp +++ /dev/null @@ -1,766 +0,0 @@ -// SPDX-FileCopyrightText: Olivier Cléro -// SPDX-License-Identifier: MIT - -#include "CsdWindow.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -CsdWindow::CsdWindow(QWidget* parent) - : oclero::qlementine::FramelessWindow(parent) { - setWindowIcon(QIcon(":/qlementine_icon.ico")); - setupUi(); - resize(600, 400); - setWindowTitle("Custom native window"); - populateMenuBar(menuBar()); -} - -void CsdWindow::paintEvent(QPaintEvent* event) { - if (_useDefaultColor) { - FramelessWindow::paintEvent(event); - } else { - QPainter painter(this); - painter.fillRect(rect(), _backgroundColor); - } -} - -void CsdWindow::setupUi() { - auto* content = new QTabWidget(this); - - auto addseg = [](const QString& caption, QWidget* parent) { - auto* segTitle = new QWidget(parent); - auto layout = new QHBoxLayout; - segTitle->setLayout(layout); - layout->addWidget(new QLabel(caption, segTitle)); - auto* hline = new QFrame(segTitle); - hline->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); - hline->setFrameShape(QFrame::HLine); - layout->addWidget(hline); - return segTitle; - }; - - // Basic Widget - { - auto* page = new QScrollArea(content); - - auto* root = new QWidget(page); - page->setWidget(root); - page->setWidgetResizable(true); - page->setAlignment(Qt::AlignHCenter); - auto* layout = new QVBoxLayout(); - root->setLayout(layout); - - // QCheckBox - { - auto* widget = new QWidget(root); - auto* llayout = new QHBoxLayout; - widget->setLayout(llayout); - - auto* checkbox = new QCheckBox("Normal", widget); - llayout->addWidget(checkbox); - - auto* checkbox2 = new QCheckBox("Disabled", widget); - checkbox2->setDisabled(true); - llayout->addWidget(checkbox2); - - auto* checkbox3 = new QCheckBox("WithIcon", widget); - checkbox3->setIcon(QIcon(QStringLiteral(":/plus_24.svg"))); - llayout->addWidget(checkbox3); - - auto* checkbox4 = new QCheckBox("NoneCheckable", widget); - checkbox4->setCheckable(false); - llayout->addWidget(checkbox4); - - auto* checkbox5 = new QCheckBox("PartiallyChecked", widget); - checkbox5->setCheckState(Qt::CheckState::PartiallyChecked); - llayout->addWidget(checkbox5); - - layout->addWidget(addseg("QCheckBox", root)); - layout->addWidget(widget); - } - - // QComboBox - { - auto createCombo = [](QWidget* parent) { - auto* widget = new QComboBox(parent); - widget->addItems({ "North", "South", "West", "East" }); - widget->addItem(QIcon(QStringLiteral(":/plus_24.svg")), "Directions"); - return widget; - }; - - auto* widget = new QWidget(root); - auto* llayout = new QHBoxLayout; - widget->setLayout(llayout); - - auto* normal = createCombo(widget); - llayout->addWidget(normal); - auto* editable = createCombo(widget); - editable->setEditable(true); - llayout->addWidget(editable); - - layout->addWidget(addseg("QComboBox", root)); - layout->addWidget(widget); - } - - // QCommandLinkButton - { - auto* widget = new QCommandLinkButton("ClickMe", "A vista style button", root); - - layout->addWidget(addseg("QCommandLinkButton", root)); - layout->addWidget(widget); - } - - // QDateEdit - { - auto* widget = new QDateEdit(root); - - layout->addWidget(addseg("QDateEdit", root)); - layout->addWidget(widget); - } - - // QDateTimeEdit - { - auto* widget = new QDateTimeEdit(root); - - layout->addWidget(addseg("QDateTimeEdit", root)); - layout->addWidget(widget); - } - - // QDial - { - auto* widget = new QDial(root); - - layout->addWidget(addseg("QDial", root)); - layout->addWidget(widget); - } - - // QDoubleSpinBox - { - auto* widget = new QDoubleSpinBox(root); - - layout->addWidget(addseg("QDoubleSpinBox", root)); - layout->addWidget(widget); - } - - // QFontComboBox - { - auto* widget = new QFontComboBox(root); - - layout->addWidget(addseg("QFontComboBox", root)); - layout->addWidget(widget); - } - - // QLCDNumber - { - auto* widget = new QLCDNumber(root); - widget->setDigitCount(1000); - widget->setMaximumWidth(200); - - layout->addWidget(addseg("QLCDNumber", root)); - layout->addWidget(widget); - } - - // QLabel - { - auto* widget = new QLabel(root); - widget->setText("This is a label"); - - layout->addWidget(addseg("QLabel", root)); - layout->addWidget(widget); - } - - // QLineEdit - { - auto* widget = new QWidget(root); - auto* llayout = new QHBoxLayout; - widget->setLayout(llayout); - - auto* normal = new QLineEdit(widget); - llayout->addWidget(normal); - auto* placeHold = new QLineEdit(widget); - placeHold->setPlaceholderText("typing..."); - llayout->addWidget(placeHold); - - layout->addWidget(addseg("QLineEdit", root)); - layout->addWidget(widget); - } - - // QMenu - { - auto* widget = new QMenuBar(root); - auto* menu = new QMenu("Menu", widget); - menu->addAction(QIcon(QStringLiteral(":/plus_24.svg")), "Item1"); - - - layout->addWidget(addseg("QMenu", root)); - layout->addWidget(widget); - } - - // QProgressBar - { - auto* widget = new QProgressBar(root); - widget->setValue(42); - - layout->addWidget(addseg("QProgressBar", root)); - layout->addWidget(widget); - } - - // QPushButton - { - auto* widget = new QWidget(root); - auto* llayout = new QHBoxLayout; - widget->setLayout(llayout); - - auto* pushButton = new QPushButton("Normal", widget); - llayout->addWidget(pushButton); - - auto* pushButton2 = new QPushButton("Disabled", widget); - pushButton2->setDisabled(true); - llayout->addWidget(pushButton2); - - auto* pushButton3 = new QPushButton("WithIcon", widget); - pushButton3->setIcon(QIcon(QStringLiteral(":/plus_24.svg"))); - llayout->addWidget(pushButton3); - - auto* pushButton4 = new QPushButton("Flat", widget); - pushButton4->setFlat(true); - llayout->addWidget(pushButton4); - - layout->addWidget(addseg("QPushButton", root)); - layout->addWidget(widget); - } - - // QRadioButton - { - auto* widget = new QWidget(root); - auto* llayout = new QHBoxLayout; - widget->setLayout(llayout); - - auto* radiobutton = new QRadioButton("Normal", widget); - llayout->addWidget(radiobutton); - - auto* radiobutton2 = new QRadioButton("Disabled", widget); - radiobutton2->setDisabled(true); - llayout->addWidget(radiobutton2); - - auto* radiobutton3 = new QRadioButton("WithIcon", widget); - radiobutton3->setIcon(QIcon(QStringLiteral(":/plus_24.svg"))); - llayout->addWidget(radiobutton3); - - auto* radiobutton4 = new QRadioButton("NoneCheckable", widget); - radiobutton4->setCheckable(false); - llayout->addWidget(radiobutton4); - - layout->addWidget(addseg("QRadioButton", root)); - layout->addWidget(widget); - } - - // QScrollBar - { - auto* widget = new QScrollBar(Qt::Horizontal, root); - - layout->addWidget(addseg("QScrollBar", root)); - layout->addWidget(widget); - } - - // QSlider - { - auto* widget = new QSlider(Qt::Horizontal, root); - auto* widget2 = new QSlider(Qt::Vertical, root); - - layout->addWidget(addseg("QSlider", root)); - layout->addWidget(widget); - layout->addWidget(widget2); - } - - // QSpinBox - { - auto* widget = new QSpinBox(root); - - layout->addWidget(addseg("QSpinBox", root)); - layout->addWidget(widget); - } - - // QTabBar - { - auto* widget = new QTabBar(root); - widget->addTab("Page1"); - widget->addTab(QIcon(QStringLiteral(":/plus_24.svg")), "Page2"); - widget->addTab(QIcon(QStringLiteral(":/plus_24.svg")), "Page3"); - widget->setExpanding(false); - - layout->addWidget(addseg("QTabBar", root)); - layout->addWidget(widget); - } - - // QTimeEdit - { - auto* widget = new QTimeEdit(root); - - layout->addWidget(addseg("QTimeEdit", root)); - layout->addWidget(widget); - } - - // QToolBox - { - auto* widget = new QToolBox(root); - widget->addItem(new QWidget(), QIcon(QStringLiteral(":/plus_24.svg")), "Item1"); - widget->addItem(new QWidget(), QIcon(QStringLiteral(":/plus_24.svg")), "Item2"); - - layout->addWidget(addseg("QToolBox", root)); - layout->addWidget(widget); - } - - // QToolButton - { - auto* widget = new QToolBar(root); - - auto icon = QIcon(QStringLiteral(":/plus_24.svg")); - - auto* toolbutton1 = new QToolButton(widget); - toolbutton1->setIcon(icon); - toolbutton1->setToolTip("with actions"); - toolbutton1->addAction(new QAction(icon, "Item1")); - toolbutton1->addAction(new QAction(icon, "Item2")); - widget->addWidget(toolbutton1); - - auto* toolbutton2 = new QToolButton(widget); - toolbutton2->setIcon(icon); - toolbutton2->setToolTip("QToolButton::DelayedPopup"); - toolbutton2->setPopupMode(QToolButton::DelayedPopup); - widget->addWidget(toolbutton2); - - auto* toolbutton3 = new QToolButton(widget); - toolbutton3->setIcon(icon); - toolbutton3->setToolTip("QToolButton::MenuButtonPopup"); - toolbutton3->setPopupMode(QToolButton::MenuButtonPopup); - widget->addWidget(toolbutton3); - - auto* toolbutton4 = new QToolButton(widget); - toolbutton4->setIcon(icon); - toolbutton4->setToolTip("QToolButton::InstantPopup"); - toolbutton4->setPopupMode(QToolButton::InstantPopup); - widget->addWidget(toolbutton4); - - auto* toolbutton5 = new QToolButton(widget); - toolbutton5->setIcon(icon); - toolbutton5->setToolTip("Qt::UpArrow"); - toolbutton5->setArrowType(Qt::UpArrow); - widget->addWidget(toolbutton5); - - auto* toolbutton6 = new QToolButton(widget); - toolbutton6->setIcon(icon); - toolbutton6->setToolTip("Qt::DownArrow"); - toolbutton6->setArrowType(Qt::DownArrow); - widget->addWidget(toolbutton6); - - auto* toolbutton7 = new QToolButton(widget); - toolbutton7->setIcon(icon); - toolbutton7->setToolTip("Qt::LeftArrow"); - toolbutton7->setArrowType(Qt::LeftArrow); - widget->addWidget(toolbutton7); - - auto* toolbutton8 = new QToolButton(widget); - toolbutton8->setIcon(icon); - toolbutton8->setToolTip("Qt::RightArrow"); - toolbutton8->setArrowType(Qt::RightArrow); - widget->addWidget(toolbutton8); - - auto* toolbutton9 = new QToolButton(widget); - toolbutton9->setIcon(icon); - toolbutton9->setToolTip("AutoRaise enabled"); - toolbutton9->setAutoRaise(true); - widget->addWidget(toolbutton9); - - layout->addWidget(addseg("QToolBox", root)); - layout->addWidget(widget); - } - - layout->addStretch(); - content->addTab(page, "Basic Widget"); - } - - - // Advanced Widget - { - auto* page = new QScrollArea(content); - - auto* root = new QWidget(page); - page->setWidget(root); - page->setWidgetResizable(true); - page->setAlignment(Qt::AlignHCenter); - auto* layout = new QVBoxLayout(); - root->setLayout(layout); - - // QCalendarWidget - { - auto* calendar = new QCalendarWidget(root); - layout->addWidget(addseg("QCalendarWidget", root)); - layout->addWidget(calendar); - } - - // QDialogButtonBox - { - auto* widget = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, root); - layout->addWidget(addseg("QDialogButtonBox", root)); - layout->addWidget(widget); - } - - - // QDialog - { - auto* widget = new QWidget(root); - auto* llayout = new QHBoxLayout; - widget->setLayout(llayout); - - auto* colorDialog = new QPushButton("QColorDialog", root); - connect(colorDialog, &QPushButton::clicked, []() { - QColorDialog dialog; - dialog.exec(); - }); - llayout->addWidget(colorDialog); - - auto* fileDialog = new QPushButton("QFileDialog", root); - connect(fileDialog, &QPushButton::clicked, []() { - QFileDialog dialog; - dialog.exec(); - }); - llayout->addWidget(fileDialog); - - auto* fontDialog = new QPushButton("QFontDialog", root); - connect(fontDialog, &QPushButton::clicked, []() { - QFontDialog dialog; - dialog.exec(); - }); - llayout->addWidget(fontDialog); - - layout->addWidget(addseg("QDialog(s)", root)); - layout->addWidget(widget); - } - - layout->addStretch(); - content->addTab(page, "Advanced Widget"); - } - - // Organizer Widget - { - auto* page = new QScrollArea(content); - - auto* root = new QWidget(page); - page->setWidget(root); - page->setWidgetResizable(true); - page->setAlignment(Qt::AlignHCenter); - auto* layout = new QVBoxLayout(); - root->setLayout(layout); - - // QButtonGroup - { - auto* buttonGroup = new QButtonGroup(root); - buttonGroup->addButton(new QCheckBox("Button1")); - buttonGroup->addButton(new QCheckBox("Button2")); - buttonGroup->setExclusive(true); - layout->addWidget(addseg("QButtonGroup(checkbox list)", root)); - for (auto* button : buttonGroup->buttons()) { - layout->addWidget(button); - } - } - - // QGroupBox - { - auto* widget = new QGroupBox("Group", root); - - auto* llayout = new QHBoxLayout; - widget->setLayout(llayout); - - llayout->addWidget(new QPushButton("Button1", widget)); - llayout->addWidget(new QPushButton("Button2", widget)); - - layout->addWidget(addseg("QGroupBox", root)); - layout->addWidget(widget); - } - - // QSplitter - { - auto* widget = new QSplitter(root); - - layout->addWidget(addseg("QSplitter", root)); - layout->addWidget(widget); - } - - // QTabWidget - { - auto* widget = new QTabWidget(root); - widget->addTab(new QLabel("Page1"), "Page1"); - widget->addTab(new QLabel("Page2"), QIcon(QStringLiteral(":/plus_24.svg")), "Page2"); - widget->addTab(new QLabel("Page3"), QIcon(QStringLiteral(":/plus_24.svg")), "Page3"); - - layout->addWidget(addseg("QTabWidget", root)); - layout->addWidget(widget); - } - - layout->addStretch(); - content->addTab(page, "Organizer Widget"); - } - - // Model/View - { - auto* page = new QScrollArea(content); - - auto* root = new QWidget(page); - page->setWidget(root); - page->setWidgetResizable(true); - page->setAlignment(Qt::AlignHCenter); - auto* layout = new QVBoxLayout(); - root->setLayout(layout); - - // QListWidget - { - auto* widget = new QListWidget(root); - - widget->addItems({ "Item1", "Item2", "Item3" }); - - layout->addWidget(addseg("QListWidget", root)); - layout->addWidget(widget); - } - - // QTableWidget - { - auto* widget = new QTableWidget(root); - - constexpr int rows = 100; - constexpr int cols = 10; - - widget->setColumnCount(cols); - widget->setRowCount(rows); - - for (int row = 0; row < rows; row++) { - for (int col = 0; col < cols; col++) { - widget->setItem(row, col, new QTableWidgetItem(QString::number(row) + ":" + QString::number(col))); - } - } - - layout->addWidget(addseg("QTableWidget", root)); - layout->addWidget(widget); - } - - // QTreeView - { - auto* widget = new QTreeView(root); - auto* model = new QFileSystemModel(root); - widget->setModel(model); - model->setRootPath(QCoreApplication::applicationDirPath()); - - layout->addWidget(addseg("QTreeView", root)); - layout->addWidget(widget); - } - - - layout->addStretch(); - content->addTab(page, "Model/View"); - } - - // Main Window - { - auto* page = new QScrollArea(content); - - auto* root = new QWidget(page); - page->setWidget(root); - page->setWidgetResizable(true); - page->setAlignment(Qt::AlignHCenter); - auto* layout = new QVBoxLayout(); - root->setLayout(layout); - - - // MainWindow - { - auto icon = QIcon(QStringLiteral(":/plus_24.svg")); - - auto* widget = new QMainWindow(); - auto* focus = new QFocusFrame(); - focus->setWidget(widget); - - auto* status = new QStatusBar(widget); - status->addWidget(new QLabel("Status1")); - status->addWidget(new QLabel("Status2"), 100); - status->addWidget(new QLabel("Status3")); - widget->setStatusBar(status); - - auto* menu = new QMenuBar(widget); - auto* fmenu = menu->addMenu("File"); - auto* emenu = menu->addMenu("Edit"); - auto* vmenu = menu->addMenu("View"); - menu->addMenu("Window"); - auto* hmenu = menu->addMenu("Help"); - widget->setMenuBar(menu); - auto* anew = fmenu->addAction(icon, "New"); - auto* aopen = fmenu->addAction(icon, "Open"); - auto* asave = fmenu->addAction(icon, "Save"); - auto* asaveas = fmenu->addAction(icon, "Save As"); - fmenu->addSeparator(); - auto* aclose = fmenu->addAction(icon, "Close"); - auto* acopy = emenu->addAction(icon, "Copy"); - auto* acut = emenu->addAction(icon, "Cut"); - auto* apaste = emenu->addAction(icon, "Paste"); - auto* ahelp = hmenu->addAction(icon, "Help"); - auto* aabout = hmenu->addAction(icon, "About"); - - auto* ftoolbar = widget->addToolBar("File"); - ftoolbar->setAllowedAreas(Qt::ToolBarArea::AllToolBarAreas); - ftoolbar->setMovable(true); - ftoolbar->setIconSize({ 24, 24 }); - ftoolbar->addActions({ anew, aopen, asave, asaveas, aclose }); - - auto* etoolbar = widget->addToolBar("Edit"); - etoolbar->setAllowedAreas(Qt::ToolBarArea::AllToolBarAreas); - etoolbar->setMovable(true); - etoolbar->setIconSize({ 24, 24 }); - etoolbar->addActions({ acopy, acut, apaste }); - - auto* htoolbar = widget->addToolBar("Help"); - htoolbar->setAllowedAreas(Qt::ToolBarArea::AllToolBarAreas); - htoolbar->setMovable(true); - htoolbar->setIconSize({ 24, 24 }); - htoolbar->addActions({ ahelp, aabout }); - - widget->setDockOptions(QMainWindow::DockOption::AllowTabbedDocks); - auto* treeview = new QTreeView(); - auto* treemodel = new QFileSystemModel(); - treeview->setModel(treemodel); - treemodel->setRootPath(QCoreApplication::applicationDirPath()); - auto* dock1 = new QDockWidget("Browser", widget); - dock1->setWidget(treeview); - dock1->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); - vmenu->addAction(dock1->toggleViewAction()); - widget->addDockWidget(Qt::RightDockWidgetArea, dock1); - - auto* docs = new QListWidget(); - docs->addItems( - QStringList() - << "A custom QStyle named QlementineStyle, that implements all the necessary API to give a modern look and feel to your Qt application. It's a drop-in replacement for the default QStyle." - << "An actual way to have client-side decoration (CSD) on your Qt window, with actual OS window animations and effects. (Windows only, at the moment)" - << "Lots of utilities to help you write beautiful Qt widgets."); - docs->setWordWrap(true); - auto* dock2 = new QDockWidget(tr("Features"), this); - dock2->setWidget(docs); - dock2->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); - vmenu->addAction(dock2->toggleViewAction()); - widget->addDockWidget(Qt::RightDockWidgetArea, dock2); - - - auto* mdiarea = new QMdiArea(widget); - widget->setCentralWidget(mdiarea); - - auto* subwnd1 = mdiarea->addSubWindow(new QTextEdit(), Qt::Window); - subwnd1->setWindowTitle("Window1"); - auto* subwnd2 = mdiarea->addSubWindow(new QTextEdit(), Qt::Window); - subwnd2->setWindowTitle("Window2"); - - layout->addWidget(widget); - } - - layout->addStretch(); - content->addTab(page, "Main Window "); - } - - // window color - { - auto* page = new QWidget(content); - auto* verticalLayout = new QVBoxLayout(page); - - // Slider to modify the window background color. - auto* slider = new QSlider(page); - slider->setRange(0, 255); - slider->setValue(_backgroundColor.red()); - QObject::connect(slider, &QSlider::valueChanged, this, [this](int value) { - _backgroundColor.setRed(value); - if (!_useDefaultColor) { - update(); - } - }); - slider->setMinimumWidth(255); - slider->setMaximumWidth(350); - verticalLayout->addWidget(slider, 0, Qt::AlignmentFlag::AlignCenter); - - // Checkbox to use or not the default background color. - auto* checkbox = new QCheckBox("Use default window color", page); - checkbox->setChecked(false); - - QObject::connect(checkbox, &QCheckBox::toggled, this, [this](bool checked) { - _useDefaultColor = checked; - update(); - }); - verticalLayout->addWidget(checkbox, 0, Qt::AlignmentFlag::AlignCenter); - - content->addTab(page, QStringLiteral("WindowColor")); - } - - setContentWidget(content); -} - -void CsdWindow::populateMenuBar(QMenuBar* menuBar) { - auto* fileMenu = new QMenu("&File", menuBar); - { - auto* quitAction = new QAction("&Quit", fileMenu); - quitAction->setMenuRole(QAction::MenuRole::QuitRole); - quitAction->setShortcut(Qt::CTRL + Qt::Key_Q); - quitAction->setShortcutContext(Qt::ShortcutContext::ApplicationShortcut); - QObject::connect(quitAction, &QAction::triggered, this, []() { - QApplication::quit(); - }); - fileMenu->addAction(quitAction); - } - - menuBar->addMenu(fileMenu); - - auto* windowMenu = new QMenu("&Window", menuBar); - { - auto minimizeAction = new QAction("Minimize", windowMenu); - QObject::connect(minimizeAction, &QAction::triggered, this, [this]() { - windowHandle()->showMinimized(); - }); - windowMenu->addAction(minimizeAction); - - auto* maximizeAction = new QAction("Maximize", windowMenu); - QObject::connect(maximizeAction, &QAction::triggered, this, [this]() { - if (auto window = this->window()) { - if (window->windowState() & Qt::WindowMaximized) { - window->showNormal(); - } else { - window->showMaximized(); - } - } - }); - windowMenu->addAction(maximizeAction); - - auto* closeAction = new QAction("&Close", windowMenu); - QObject::connect(closeAction, &QAction::triggered, this, [this]() { - windowHandle()->close(); - }); - windowMenu->addAction(closeAction); - } - menuBar->addMenu(windowMenu); - - auto* helpMenu = new QMenu("&Help", menuBar); - { - auto* aboutAction = new QAction("&About", windowMenu); - aboutAction->setMenuRole(QAction::AboutRole); - QObject::connect(aboutAction, &QAction::triggered, this, [this]() { - QMessageBox msgBox( - QMessageBox::Icon::Information, "About", "Example of frameless window", QMessageBox::NoButton, this); - msgBox.exec(); - }); - helpMenu->addAction(aboutAction); - } - menuBar->addMenu(helpMenu); -} diff --git a/sandbox/src/CsdWindow.hpp b/sandbox/src/CsdWindow.hpp deleted file mode 100644 index 81bc455..0000000 --- a/sandbox/src/CsdWindow.hpp +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: Olivier Cléro -// SPDX-License-Identifier: MIT - -#pragma once - -#include - -class CsdWindow : public oclero::qlementine::FramelessWindow { - Q_OBJECT - -public: - explicit CsdWindow(QWidget* parent = nullptr); - ~CsdWindow() = default; - -protected: - void paintEvent(QPaintEvent* event) override; - -private: - void populateMenuBar(QMenuBar* menuBar); - void setupUi(); - -private: - QColor _backgroundColor{ 255, 192, 0 }; - bool _useDefaultColor{ false }; -}; diff --git a/sandbox/src/SandboxWindow.cpp b/sandbox/src/SandboxWindow.cpp index efd0b11..05609d0 100644 --- a/sandbox/src/SandboxWindow.cpp +++ b/sandbox/src/SandboxWindow.cpp @@ -4,10 +4,13 @@ #include "SandboxWindow.hpp" #include +#include #include #include #include #include +#include +#include #include #include #include @@ -50,10 +53,24 @@ #include #include #include +#include #include namespace oclero::qlementine::sandbox { + +static QIcon getTestQIcon(const QSize& size = { 16, 16 }, bool colored = false) { + if (size.height() == 24) { + return QIcon(":/sandbox/test_image_24x24.svg"); + } else { + return colored ? QIcon(":/sandbox/test_image_color_16x16.svg") : QIcon(":/sandbox/test_image_16x16.svg"); + } +} + +// static QIcon makeQIcon(Icons16 id, const QSize& size = { 16, 16 }) { +// return oclero::qlementine::makeThemedIcon(id, size); +// } + class ContextMenuEventFilter : public QObject { private: std::function _cb; @@ -206,8 +223,17 @@ class CustomBgWidget : public QWidget { }; struct SandboxWindow::Impl { - Impl(SandboxWindow& o) - : owner(o) {} + SandboxWindow& owner; + QPointer themeManager{ nullptr }; + + QWidget* windowContent{ nullptr }; + QBoxLayout* windowContentLayout{ nullptr }; + QScrollArea* globalScrollArea{ nullptr }; + QToolBar* toolbar{ nullptr }; + + Impl(SandboxWindow& o, ThemeManager* themeManager) + : owner(o) + , themeManager(themeManager) {} void beginSetupUI() { // Create a scrollarea to wrap everything §the window can be quite huge). @@ -243,20 +269,16 @@ struct SandboxWindow::Impl { } }); - auto* themeShortcut = new QShortcut(Qt::CTRL | Qt::Key_T, &owner); - themeShortcut->setAutoRepeat(false); - themeShortcut->setContext(Qt::ShortcutContext::ApplicationShortcut); - QObject::connect(themeShortcut, &QShortcut::activated, themeShortcut, [this]() { - const auto light = QStringLiteral(":/light.json"); - const auto dark = QStringLiteral(":/dark.json"); - if (lastJsonThemePath == dark) { - lastJsonThemePath = light; - qlementineStyle->setThemeJsonPath(light); - } else { - lastJsonThemePath = dark; - qlementineStyle->setThemeJsonPath(dark); - } - }); + if (themeManager) { + auto* themeShortcut = new QShortcut(Qt::CTRL | Qt::Key_T, &owner); + themeShortcut->setAutoRepeat(false); + themeShortcut->setContext(Qt::ShortcutContext::ApplicationShortcut); + QObject::connect(themeShortcut, &QShortcut::activated, themeShortcut, [this]() { + if (themeManager) { + themeManager->setNextTheme(); + } + }); + } auto* focusShortcut = new QShortcut(Qt::CTRL | Qt::Key_F, &owner); focusShortcut->setAutoRepeat(false); @@ -336,7 +358,7 @@ struct SandboxWindow::Impl { void setupUI_button() { auto* button = new QPushButton(windowContent); button->setText("Button with a very long text that can be elided"); - button->setIcon(QIcon(":/refresh.svg")); + button->setIcon(getTestQIcon()); button->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); button->setDefault(true); windowContentLayout->addWidget(button); @@ -354,7 +376,7 @@ struct SandboxWindow::Impl { { // Icon, fixed size auto* button = new QPushButton(windowContent); - button->setIcon(QIcon(":/refresh.svg")); + button->setIcon(getTestQIcon()); button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); windowContentLayout->addWidget(button); } @@ -362,7 +384,7 @@ struct SandboxWindow::Impl { // Text+Icon, fixed size auto* button = new QPushButton(windowContent); button->setText("Button"); - button->setIcon(QIcon(":/refresh.svg")); + button->setIcon(getTestQIcon()); button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); windowContentLayout->addWidget(button); } @@ -370,7 +392,7 @@ struct SandboxWindow::Impl { // Text+Icon+Menu, fixed size auto* button = new QPushButton(windowContent); button->setText("Button"); - button->setIcon(QIcon(":/refresh.svg")); + button->setIcon(getTestQIcon()); button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); auto* menu = new QMenu(button); @@ -390,21 +412,21 @@ struct SandboxWindow::Impl { { // Icon, expanding size. auto* button = new QPushButton(windowContent); - button->setIcon(QIcon(":/refresh.svg")); + button->setIcon(getTestQIcon()); windowContentLayout->addWidget(button); } { // Text+Icon, expanding size. auto* button = new QPushButton(windowContent); button->setText("Button"); - button->setIcon(QIcon(":/refresh.svg")); + button->setIcon(getTestQIcon()); windowContentLayout->addWidget(button); } { // Text+Icon+Menu, expanding size auto* button = new QPushButton(windowContent); button->setText("Button"); - button->setIcon(QIcon(":/refresh.svg")); + button->setIcon(getTestQIcon()); auto* menu = new QMenu("ButtonMenu"); for (auto i = 0; i < 3; ++i) { @@ -422,7 +444,7 @@ struct SandboxWindow::Impl { const auto tristate = i > 1; checkbox->setChecked(checked); - checkbox->setIcon(QIcon(":/refresh.svg")); + checkbox->setIcon(getTestQIcon()); checkbox->setText(QString("%1 checkbox %2 with a very long text").arg(tristate ? "Tristate" : "Normal").arg(i)); checkbox->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); checkbox->setTristate(tristate); @@ -436,7 +458,7 @@ struct SandboxWindow::Impl { for (auto i = 0; i < 2; ++i) { auto* radiobutton = new QRadioButton(windowContent); radiobutton->setChecked(true); - radiobutton->setIcon(QIcon(":/refresh.svg")); + radiobutton->setIcon(getTestQIcon()); radiobutton->setText(QString("RadioButton %1 with a very long text").arg(i)); radiobutton->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); radioGroup->addButton(radiobutton); @@ -446,21 +468,19 @@ struct SandboxWindow::Impl { void setupUI_commandLinkButton() { { - const QIcon icon(":/plus_24.svg"); auto* button = new CommandLinkButton(windowContent); button->setText("First Line with a very long text that should be cropped"); button->setDescription("Second Line that could be very long and should be cropped"); - button->setIcon(icon); + button->setIcon(getTestQIcon({ 24, 24 })); button->setDefault(true); button->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); windowContentLayout->addWidget(button); } { - const QIcon icon(":/plus_24.svg"); auto* button = new CommandLinkButton(windowContent); button->setText("First Line with a very long text that should be cropped"); button->setDescription("Second Line that could be very long and should be cropped"); - button->setIcon(icon); + button->setIcon(getTestQIcon({ 24, 24 })); button->setDefault(false); button->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); windowContentLayout->addWidget(button); @@ -586,7 +606,7 @@ struct SandboxWindow::Impl { combobox->setEditable(true); for (auto i = 0; i < 4; ++i) { - combobox->addItem(QIcon(":/refresh.svg"), QString("Editable comboBox item %1").arg(i)); + combobox->addItem(getTestQIcon(), QString("Editable comboBox item %1").arg(i)); } auto* model = qobject_cast(combobox->model()); auto* item = model->item(2); @@ -597,26 +617,33 @@ struct SandboxWindow::Impl { // Non-editable { auto* combobox = new QComboBox(windowContent); - combobox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + combobox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); combobox->setFocusPolicy(Qt::StrongFocus); for (auto i = 0; i < 4; ++i) { - combobox->addItem(QIcon(":/refresh.svg"), QString("ComboBox item %1").arg(i)); + combobox->addItem(getTestQIcon(), QString("ComboBox item %1").arg(i)); } windowContentLayout->addWidget(combobox); } } + void setupUI_fontComboBox() { + auto* combobox = new QFontComboBox(windowContent); + combobox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + combobox->setFocusPolicy(Qt::StrongFocus); + windowContentLayout->addWidget(combobox); + } + void setupUI_listView() { auto* listView = new QListWidget(windowContent); listView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); //listView->setAlternatingRowColors(true); listView->setIconSize(QSize(32, 32)); - for (auto i = 0; i < 6; ++i) { + for (auto i = 0; i < 2; ++i) { auto* item = new QListWidgetItem( - QIcon(":/refresh.svg"), QString("Item #%1 with very long text that can be elided").arg(i), listView); + getTestQIcon(), QString("Item #%1 with very long text that can be elided").arg(i), listView); item->setFlags(item->flags() | Qt::ItemFlag::ItemIsUserCheckable); item->setCheckState(i % 2 ? Qt ::CheckState::Checked : Qt::CheckState::Unchecked); //item->setForeground(i % 2 ? Qt::red : Qt::blue); @@ -626,7 +653,6 @@ struct SandboxWindow::Impl { windowContentLayout->addWidget(listView); // Context menu. - qDebug() << listView->contextMenuPolicy(); listView->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); QObject::connect(listView, &QListView::customContextMenuRequested, listView, [listView](const QPoint& pos) { if (const auto item = listView->itemAt(pos)) { @@ -660,7 +686,6 @@ struct SandboxWindow::Impl { constexpr auto rowCount = 3; tableView->setColumnCount(columnCount); tableView->setRowCount(rowCount); - const QIcon icon(":/scene_object.svg"); std::vector columnAlignments; columnAlignments.resize(columnCount); @@ -671,21 +696,21 @@ struct SandboxWindow::Impl { for (auto col = 0; col < columnCount; ++col) { auto* item = new QTableWidgetItem(QString("Column %1").arg(col + 1)); - item->setIcon(icon); + item->setIcon(getTestQIcon({ 16, 16 }, true)); item->setTextAlignment(columnAlignments.at(col)); tableView->setHorizontalHeaderItem(col, item); } for (auto row = 0; row < rowCount; ++row) { auto* item = new QTableWidgetItem(QString("Row %1").arg(row + 1)); - item->setIcon(icon); + item->setIcon(getTestQIcon({ 16, 16 }, true)); tableView->setVerticalHeaderItem(row, item); } for (auto row = 0; row < rowCount; ++row) { for (auto col = 0; col < columnCount; ++col) { auto* item = new QTableWidgetItem(QString("Item at %1, %2").arg(row + 1).arg(col + 1)); - item->setIcon(icon); + item->setIcon(getTestQIcon({ 16, 16 }, true)); item->setTextAlignment(columnAlignments.at(col)); item->setFlags(Qt::ItemFlag::ItemIsEditable | Qt::ItemFlag::ItemIsSelectable | Qt::ItemFlag::ItemIsEnabled); item->setData(Qt::DisplayRole, QVariant::fromValue(true)); @@ -703,37 +728,39 @@ struct SandboxWindow::Impl { treeWidget->setColumnCount(1); treeWidget->setHeaderHidden(true); treeWidget->setSelectionBehavior(QAbstractItemView::SelectRows); - qlementineStyle->setAutoIconColor(treeWidget, oclero::qlementine::AutoIconColor::None); + + oclero::qlementine::appStyle()->setAutoIconColor(treeWidget, oclero::qlementine::AutoIconColor::None); for (auto i = 0; i < 3; ++i) { auto* root = new QTreeWidgetItem(treeWidget); root->setText(0, QString("Root %1").arg(i + 1)); - root->setIcon(0, QIcon(":/scene_object.svg")); + root->setIcon(0, getTestQIcon({ 16, 16 }, true)); root->setText(1, QString("Column 2 of Root %1").arg(i + 1)); for (auto j = 0; j < 3; ++j) { auto* child = new QTreeWidgetItem(root); child->setText(0, QString("Child %1 of Root %2").arg(j).arg(i)); - child->setIcon(0, j == 2 ? QIcon(":/scene_light.svg") : QIcon(":/scene_object.svg")); + child->setIcon(0, getTestQIcon({ 16, 16 }, true)); child->setText(1, QString("Column 2 of Child %1 of Root %2").arg(j).arg(i)); for (auto k = 0; k < 3; ++k) { auto* subChild = new QTreeWidgetItem(child); subChild->setText(0, QString("Child %1 of Child %2 of Root %3").arg(k).arg(j).arg(i)); - subChild->setIcon(0, QIcon(":/scene_material.svg")); + subChild->setIcon(0, getTestQIcon({ 16, 16 }, true)); subChild->setText(1, QString("Column 2 of Child %1 of Child %2 of Root %3").arg(k).arg(j).arg(i)); } } } - treeWidget->topLevelItem(0)->setSelected(true), windowContentLayout->addWidget(treeWidget); + treeWidget->topLevelItem(0)->setSelected(true); + windowContentLayout->addWidget(treeWidget); } void setupUI_menuBar() const { auto* menuBar = owner.menuBar(); // NB: it looks like MacOS' native menu bar has an issue with QIcon, so we have to force // it to generate icons for High-DPI screens. - const auto icon = makeIconFromSvg(":/refresh.svg", owner.iconSize()); + const auto icon = getTestQIcon(); for (auto i = 0; i < 5; ++i) { auto* menu = menuBar->addMenu(QString("Menu &%1").arg(i)); @@ -776,14 +803,14 @@ struct SandboxWindow::Impl { void setupUI_toolButton() { auto* toolButton = new QToolButton(toolbar); - toolButton->setIcon(QIcon(":/refresh.svg")); + toolButton->setIcon(getTestQIcon()); toolButton->setText(QString("Button with a very long text that can be elided")); toolButton->setToolButtonStyle(Qt::ToolButtonIconOnly); toolButton->setCheckable(false); toolButton->setChecked(false); { - const auto icon = QIcon(":/refresh.svg"); + const auto icon = getTestQIcon(); auto* subMenu = new QMenu("Menu title", toolButton); toolButton->setMenu(subMenu); subMenu->addAction(icon, "Sub Action 1"); @@ -797,7 +824,7 @@ struct SandboxWindow::Impl { } void setupUI_toolButtonsVariants() { - const auto icon = QIcon(":/refresh.svg"); + const auto icon = getTestQIcon(); toolbar = owner.addToolBar("ToolBar name"); //toolbar->set @@ -884,11 +911,10 @@ struct SandboxWindow::Impl { } void setupUI_tabBar() { - const QIcon icon(":/scene_object.svg"); auto* tabBar = new QTabBar(windowContent); tabBar->setFocusPolicy(Qt::NoFocus); tabBar->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - qlementineStyle->setAutoIconColor(tabBar, oclero::qlementine::AutoIconColor::None); + oclero::qlementine::appStyle()->setAutoIconColor(tabBar, oclero::qlementine::AutoIconColor::None); // QTabBar features. tabBar->setTabsClosable(true); @@ -909,7 +935,7 @@ struct SandboxWindow::Impl { const auto tabText = QString(tabTextList.join(" ").append(QString(" %1").arg(i + 1))); if (i % 3 == 0) { - tabBar->addTab(icon, tabText); + tabBar->addTab(getTestQIcon({ 16, 16 }, true), tabText); } else { tabBar->addTab(tabText); } @@ -935,7 +961,6 @@ struct SandboxWindow::Impl { } void setupUI_tabWidget() { - const QStringList icons = { ":/scene_object.svg", ":/scene_light.svg", ":/scene_material.svg" }; auto* tabWidget = new QTabWidget(windowContent); tabWidget->setDocumentMode(false); @@ -964,7 +989,7 @@ struct SandboxWindow::Impl { tabTextList.append("Tab"); } const auto tabText = QString(tabTextList.join(" ").append(QString(" %1").arg(i + 1))); - const auto icon = QIcon(icons.at(i % icons.size())); + const auto icon = getTestQIcon(); tabWidget->addTab(tabContent, icon, tabText); } } @@ -1067,24 +1092,24 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru treeWidget->setHeaderHidden(true); treeWidget->setSelectionBehavior(QAbstractItemView::SelectRows); treeWidget->setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection); - qlementineStyle->setAutoIconColor(treeWidget, oclero::qlementine::AutoIconColor::None); + oclero::qlementine::appStyle()->setAutoIconColor(treeWidget, oclero::qlementine::AutoIconColor::None); for (auto i = 0; i < 3; ++i) { auto* root = new QTreeWidgetItem(treeWidget); root->setText(0, QString("Root %1").arg(i + 1)); - root->setIcon(0, QIcon(":/scene_object.svg")); + root->setIcon(0, getTestQIcon({ 16, 16 }, true)); root->setText(1, QString("Column 2 of Root %1").arg(i + 1)); for (auto j = 0; j < 3; ++j) { auto* child = new QTreeWidgetItem(root); child->setText(0, QString("Child %1 of Root %2").arg(j).arg(i)); - child->setIcon(0, j == 2 ? QIcon(":/scene_light.svg") : QIcon(":/scene_object.svg")); + child->setIcon(0, getTestQIcon({ 16, 16 }, true)); child->setText(1, QString("Column 2 of Child %1 of Root %2").arg(j).arg(i)); for (auto k = 0; k < 3; ++k) { auto* subChild = new QTreeWidgetItem(child); subChild->setText(0, QString("Child %1 of Child %2 of Root %3").arg(k).arg(j).arg(i)); - subChild->setIcon(0, QIcon(":/scene_material.svg")); + subChild->setIcon(0, getTestQIcon({ 16, 16 }, true)); subChild->setText(1, QString("Column 2 of Child %1 of Child %2 of Root %3").arg(k).arg(j).arg(i)); } } @@ -1102,7 +1127,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru for (auto i = 0; i < 3; ++i) { auto* item = new QListWidgetItem( - QIcon(":/refresh.svg"), QString("Item #%1 with very long text that can be elided").arg(i), listView); + getTestQIcon(), QString("Item #%1 with very long text that can be elided").arg(i), listView); item->setFlags(item->flags() | Qt::ItemFlag::ItemIsUserCheckable); item->setCheckState(i % 2 ? Qt ::CheckState::Checked : Qt::CheckState::Unchecked); @@ -1120,7 +1145,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru tableView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); tableView->setColumnCount(columnCount); tableView->setRowCount(rowCount); - QIcon icon(":/refresh.svg"); + auto icon = getTestQIcon(); auto* headerItem = new QTableWidgetItem(icon, "A veeeeeery long header label"); tableView->setHorizontalHeaderItem(0, headerItem); tableView->setSelectionBehavior(QTableView::SelectionBehavior::SelectRows); @@ -1151,29 +1176,54 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru windowContent->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); { auto* container = new CustomBgWidget(windowContent); + container->bgColor = QColor{ 255, 0, 0, 10 }; + container->borderColor = QColor{ 255, 0, 0, 40 }; auto* containerLayout = new QVBoxLayout(container); containerLayout->setContentsMargins(10, 10, 10, 10); container->setLayout(containerLayout); auto* expander = new Expander(container); + expander->setOrientation(Qt::Orientation::Horizontal); auto* expanderContent = new CustomBgWidget(expander); - expanderContent->bgColor = QColor{ 255, 127, 0 }; + expanderContent->bgColor = QColor{ 0, 0, 255, 40 }; + expanderContent->borderColor = QColor{ 0, 0, 255, 127 }; expanderContent->customSizeHint = QSize{ 150, 100 }; expanderContent->showBounds = true; expander->setContent(expanderContent); auto* checkBox = new QCheckBox("Expanded", container); + checkBox->setChecked(expander->expanded()); QObject::connect(checkBox, &QCheckBox::toggled, &owner, [expander](bool checked) { expander->setExpanded(checked); }); - auto* button = new QPushButton("Increase content height", container); - QObject::connect(button, &QPushButton::clicked, &owner, [expanderContent]() { - expanderContent->customSizeHint.rheight() += 20; + auto* vLayout = new QVBoxLayout(); + auto* buttonGroup = new QButtonGroup(windowContent); + for (auto orientation : { Qt::Vertical, Qt::Horizontal }) { + auto* radioButton = new QRadioButton(orientation == Qt::Vertical ? "Vertical" : "Horizontal", container); + radioButton->setChecked(orientation == expander->orientation()); + buttonGroup->addButton(radioButton); + vLayout->addWidget(radioButton); + QObject::connect(radioButton, &QRadioButton::toggled, &owner, [expander, orientation](bool checked) { + if (checked) { + expander->setOrientation(orientation); + } + }); + } + + auto* button = new QPushButton("Increase content animated dimension", container); + QObject::connect(button, &QPushButton::clicked, &owner, [expanderContent, expander]() { + if (expander->orientation() == Qt::Vertical) { + expanderContent->customSizeHint.rheight() += 20; + } else { + expanderContent->customSizeHint.rwidth() += 20; + } expanderContent->updateGeometry(); }); + containerLayout->addWidget(checkBox); containerLayout->addWidget(button); + containerLayout->addLayout(vLayout); containerLayout->addWidget(expander); windowContentLayout->addWidget(container); @@ -1320,7 +1370,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru } void setupUI_navigationBar() { - const QIcon dummyIcon(":/refresh.svg"); + const auto dummyIcon = getTestQIcon(); auto* navBar = new NavigationBar(windowContent); for (auto i = 0; i < 3; ++i) { @@ -1336,7 +1386,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru } void setupUI_switch() { - const QIcon dummyIcon(":/refresh.svg"); + const auto dummyIcon = getTestQIcon(); auto* switchWidget = new Switch(windowContent); switchWidget->setText("Label of the Switch"); switchWidget->setIcon(dummyIcon); @@ -1435,7 +1485,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru } void setupUI_lineEditStatus() { - const QIcon dummyIcon(":/refresh.svg"); + const auto dummyIcon = getTestQIcon(); auto* lineEdit = new LineEdit(windowContent); lineEdit->setText("Label of the Switch"); @@ -1553,78 +1603,66 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru windowContentLayout->addWidget(plainWidget); } - - SandboxWindow& owner; - QString lastJsonThemePath; - QPointer qlementineStyle; - - QWidget* windowContent{ nullptr }; - QBoxLayout* windowContentLayout{ nullptr }; - QScrollArea* globalScrollArea{ nullptr }; - QToolBar* toolbar{ nullptr }; }; -SandboxWindow::SandboxWindow(QWidget* parent) +SandboxWindow::SandboxWindow(ThemeManager* themeManager, QWidget* parent) : QMainWindow(parent) - , _impl(new Impl(*this)) { - setWindowIcon(QIcon(QStringLiteral(":/qlementine_icon.ico"))); + , _impl(new Impl(*this, themeManager)) { + setWindowIcon(QIcon(QStringLiteral(":/sandbox/qlementine_icon.ico"))); _impl->beginSetupUI(); { // Uncomment the line to show the corresponding widget. - // _impl->setupUI_label(); - // _impl->setupUI_button(); - // _impl->setupUI_buttonVariants(); - // _impl->setupUI_checkbox(); - // _impl->setupUI_radioButton(); - // _impl->setupUI_commandLinkButton(); - // _impl->setupUI_sliderAndProgressBar(); - // _impl->setupUI_sliderWithTicks(); - // _impl->setupUI_lineEdit(); - // _impl->setupUI_textEdit(); - // _impl->setupUI_plainTextEdit(); - // _impl->setupUI_dial(); - // _impl->setupUI_spinBox(); - // _impl->setupUI_comboBox(); - // _impl->setupUI_listView(); - // _impl->setupUI_treeWidget(); - // _impl->setupUI_table(); - // _impl->setupUI_menuBar(); - // _impl->setupUI_toolButton(); - // _impl->setupUI_toolButtonsVariants(); - // _impl->setupUI_tabBar(); - // _impl->setupUI_tabWidget(); - // _impl->setupUI_groupBox(); - // _impl->setupUI_treeView(); - // _impl->setupUI_focus(); - // _impl->setupUI_specialProgressBar(); - // _impl->setupUI_lineEditStatus(); - // _impl->setupUI_dateTimeEdit(); - // _impl->setupUI_contextMenu(); - - // _impl->setupUI_switch(); - // _impl->setupUI_expander(); - // _impl->setupUI_popover(); - // _impl->setupUI_navigationBar(); - // _impl->setupUI_badge(); - // _impl->setupUI_colorButton(); - - // _impl->setupUI_messageBoxIcons(); - // _impl->setupUI_fontMetricsTests(); - // _impl->setupUI_blur(); - // _impl->setupUI_themeEditor(); - // _impl->setupUI_messageBox(); + // _impl->setupUI_label(); + // _impl->setupUI_button(); + // _impl->setupUI_buttonVariants(); + // _impl->setupUI_checkbox(); + // _impl->setupUI_radioButton(); + // _impl->setupUI_commandLinkButton(); + // _impl->setupUI_sliderAndProgressBar(); + // _impl->setupUI_sliderWithTicks(); + // _impl->setupUI_lineEdit(); + // _impl->setupUI_textEdit(); + // _impl->setupUI_plainTextEdit(); + // _impl->setupUI_dial(); + // _impl->setupUI_spinBox(); + // _impl->setupUI_comboBox(); + // _impl->setupUI_listView(); + // _impl->setupUI_treeWidget(); + // _impl->setupUI_table(); + // _impl->setupUI_menuBar(); + // _impl->setupUI_toolButton(); + // _impl->setupUI_toolButtonsVariants(); + // _impl->setupUI_tabBar(); + // _impl->setupUI_tabWidget(); + // _impl->setupUI_groupBox(); + // _impl->setupUI_treeView(); + // _impl->setupUI_focus(); + // _impl->setupUI_specialProgressBar(); + // _impl->setupUI_lineEditStatus(); + // _impl->setupUI_dateTimeEdit(); + // _impl->setupUI_contextMenu(); + // _impl->setupUI_fontComboBox(); + + // _impl->setupUI_switch(); + // _impl->setupUI_expander(); + // _impl->setupUI_popover(); + // _impl->setupUI_navigationBar(); + // _impl->setupUI_badge(); + // _impl->setupUI_colorButton(); + // _impl->setupUI_messageBoxIcons(); + + // _impl->setupUI_fontMetricsTests(); + // _impl->setupUI_blur(); + // _impl->setupUI_themeEditor(); + // _impl->setupUI_messageBox(); } _impl->endSetupUI(); + oclero::qlementine::centerWidget(this); } SandboxWindow::~SandboxWindow() = default; -void SandboxWindow::setCustomStyle(QlementineStyle* style) { - _impl->qlementineStyle = style; - _impl->lastJsonThemePath = QStringLiteral(":/light.json"); -} - bool SandboxWindow::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::Type::Close) { qApp->closeAllWindows(); diff --git a/sandbox/src/SandboxWindow.hpp b/sandbox/src/SandboxWindow.hpp index 4c1a4ea..4acbc24 100644 --- a/sandbox/src/SandboxWindow.hpp +++ b/sandbox/src/SandboxWindow.hpp @@ -7,16 +7,15 @@ namespace oclero::qlementine { class QlementineStyle; -} +class ThemeManager; +} // namespace oclero::qlementine namespace oclero::qlementine::sandbox { class SandboxWindow : public QMainWindow { public: - SandboxWindow(QWidget* parent = nullptr); + SandboxWindow(ThemeManager* themeManager = nullptr, QWidget* parent = nullptr); ~SandboxWindow(); - void setCustomStyle(QlementineStyle* style); - bool eventFilter(QObject* watched, QEvent* event) override; private: diff --git a/sandbox/src/main.cpp b/sandbox/src/main.cpp index 15ab031..cc0dd0c 100644 --- a/sandbox/src/main.cpp +++ b/sandbox/src/main.cpp @@ -2,17 +2,13 @@ // SPDX-License-Identifier: MIT #include -#include -#include #include -#include +#include #include "SandboxWindow.hpp" -//#include "CsdWindow.hpp" #define USE_CUSTOM_STYLE 1 -//#define CSD_WINDOW 0 int main(int argc, char* argv[]) { // Must be set before creating a QApplication. @@ -21,30 +17,30 @@ int main(int argc, char* argv[]) { QApplication qApplication(argc, argv); // Must be set after creating a QApplication. - QGuiApplication::setApplicationDisplayName("sandbox"); - QCoreApplication::setApplicationName("sandbox"); - QGuiApplication::setDesktopFileName("sandbox"); + QGuiApplication::setApplicationDisplayName("Sandbox"); + QCoreApplication::setApplicationName("Sandbox"); + QGuiApplication::setDesktopFileName("Sandbox"); QCoreApplication::setOrganizationName("oclero"); QCoreApplication::setOrganizationDomain("olivierclero.com"); QCoreApplication::setApplicationVersion("1.0.0"); QApplication::setWindowIcon(QIcon(QStringLiteral(":/qlementine_icon.ico"))); - // Set custom QStyle. #if USE_CUSTOM_STYLE - auto* const style = new oclero::qlementine::QlementineStyle(&qApplication); + // Set custom QStyle. + auto* style = new oclero::qlementine::QlementineStyle(&qApplication); style->setAnimationsEnabled(true); - style->setUseMenuForComboBoxPopup(false); style->setAutoIconColor(oclero::qlementine::AutoIconColor::TextColor); - style->setThemeJsonPath(QStringLiteral(":/light.json")); qApplication.setStyle(style); -#endif - auto window = std::make_unique(); -#if USE_CUSTOM_STYLE - window->setCustomStyle(style); + // Theme manager. + auto* themeManager = new oclero::qlementine::ThemeManager(style); + themeManager->loadDirectory(":/showcase/themes"); + + // Define theme on QStyle. + themeManager->setCurrentTheme("Light"); #endif - oclero::qlementine::centerWidget(window.get()); + auto window = std::make_unique(themeManager); window->show(); return qApplication.exec(); diff --git a/scripts/format.sh b/scripts/format.sh index a80debe..b32a2e1 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,3 +1,3 @@ #!/bin/bash -find $(dirname "$0")/../lib/src $(dirname "$0")/../lib/include -iname *.hpp -o -iname *.cpp | xargs clang-format -i +find $(dirname "$0")/../lib/src $(dirname "$0")/../lib/include $(dirname "$0")/../sandbox/src $(dirname "$0")/../showcase/src -iname *.hpp -o -iname *.cpp | xargs clang-format -i diff --git a/showcase/CMakeLists.txt b/showcase/CMakeLists.txt new file mode 100644 index 0000000..ca1a1aa --- /dev/null +++ b/showcase/CMakeLists.txt @@ -0,0 +1,66 @@ +set(SHOWCASE_NAME "showcase") + +# Dependency: qlementine-icons +include(FetchContent) +FetchContent_Declare(qlementine_icons + GIT_REPOSITORY "https://github.com/oclero/qlementine-icons.git" + GIT_TAG 0a269d7c4eb77fdc8ca92a7fb2ceae1baae29727 #v1.5.0 +) +FetchContent_MakeAvailable(qlementine_icons) +set_target_properties(qlementine_icons + PROPERTIES + FOLDER dependencies +) + +if(APPLE) + set(APP_ICON_MACOS "${CMAKE_SOURCE_DIR}/branding/icon/icon.icns") + set_source_files_properties(${APP_ICON_MACOS} + PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" + ) +endif() + +set(SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ShowcaseWindow.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ShowcaseWindow.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/resources/showcase.qrc +) + +qt_add_executable(${SHOWCASE_NAME} + WIN32 MACOSX_BUNDLE + ${SOURCES} +) + +target_link_libraries(${SHOWCASE_NAME} + PUBLIC + oclero::qlementine + oclero::qlementine_icons +) + +install( + TARGETS ${SHOWCASE_NAME} + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +set_target_properties(${SHOWCASE_NAME} + PROPERTIES + INTERNAL_CONSOLE OFF + EXCLUDE_FROM_ALL OFF + FOLDER "tools" + CMAKE_AUTOMOC ON + CMAKE_AUTORCC ON + CMAKE_AUTOUIC ON + MACOSX_BUNDLE_GUI_IDENTIFIER "oclero.qlementine.${SHOWCASE_NAME}" + MACOSX_BUNDLE_BUNDLE_NAME "Showcase" + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_ICON_FILE "icon.icns" + MACOSX_BUNDLE_COPYRIGHT ${PROJECT_COPYRIGHT} + XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "${XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED}" + XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY}" + XCODE_ATTRIBUTE_CODE_SIGN_STYLE "${XCODE_ATTRIBUTE_CODE_SIGN_STYLE}" + XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS OFF +) diff --git a/showcase/resources/icons/cube-green.svg b/showcase/resources/icons/cube-green.svg new file mode 100644 index 0000000..c3c2b4a --- /dev/null +++ b/showcase/resources/icons/cube-green.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/showcase/resources/icons/cube-red.svg b/showcase/resources/icons/cube-red.svg new file mode 100644 index 0000000..de7c25a --- /dev/null +++ b/showcase/resources/icons/cube-red.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/showcase/resources/icons/cube-yellow.svg b/showcase/resources/icons/cube-yellow.svg new file mode 100644 index 0000000..48e79d2 --- /dev/null +++ b/showcase/resources/icons/cube-yellow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/showcase/resources/qlementine_icon.icns b/showcase/resources/qlementine_icon.icns new file mode 100644 index 0000000..32d7547 Binary files /dev/null and b/showcase/resources/qlementine_icon.icns differ diff --git a/showcase/resources/qlementine_icon.ico b/showcase/resources/qlementine_icon.ico new file mode 100644 index 0000000..49774ac Binary files /dev/null and b/showcase/resources/qlementine_icon.ico differ diff --git a/showcase/resources/showcase.qrc b/showcase/resources/showcase.qrc new file mode 100644 index 0000000..55f4ce2 --- /dev/null +++ b/showcase/resources/showcase.qrc @@ -0,0 +1,11 @@ + + + icons/cube-green.svg + icons/cube-red.svg + icons/cube-yellow.svg + themes/dark.json + themes/light.json + qlementine_icon.ico + qlementine_icon.icns + + diff --git a/showcase/resources/themes/dark.json b/showcase/resources/themes/dark.json new file mode 100644 index 0000000..5921357 --- /dev/null +++ b/showcase/resources/themes/dark.json @@ -0,0 +1,91 @@ +{ + "meta": { + "author": "Olivier Cléro", + "name": "Dark", + "version": "1.5.0" + }, + + "backgroundColorMain1": "#1f2127", + "backgroundColorMain2": "#282b33", + "backgroundColorMain3": "#333848", + "backgroundColorMain4": "#333848", + + "backgroundColorWorkspace": "#17181c", + "backgroundColorTabBar": "#1e2026", + + "borderColor": "#40485a", + "borderColorHovered": "#4a5670", + "borderColorPressed": "#5b6e98", + "borderColorDisabled": "#2f343f", + + "focusColor": "#3097ff6a", + + "neutralColor": "#4c5368", + "neutralColorHovered": "#535c78", + "neutralColorPressed": "#5b6688", + "neutralColorDisabled": "#2d313b", + + "primaryColor": "#5086ff", + "primaryColorHovered": "#6494ff", + "primaryColorPressed": "#7aa3ff", + "primaryColorDisabled": "#2c3448", + + "primaryAlternativeColor": "#3161f8", + "primaryAlternativeColorHovered": "#4571fe", + "primaryAlternativeColorPressed": "#5a82ff", + "primaryAlternativeColorDisabled": "#293346", + + "primaryColorForeground": "#ffffff", + "primaryColorForegroundHovered": "#ffffff", + "primaryColorForegroundPressed": "#ffffff", + "primaryColorForegroundDisabled": "#455170", + + "secondaryColor": "#ffffff", + "secondaryColorHovered": "#d5d5d5", + "secondaryColorPressed": "#ebebeb", + "secondaryColorDisabled": "#ffffff33", + + "secondaryAlternativeColor": "#67718d", + "secondaryAlternativeColorHovered": "#8b93ab", + "secondaryAlternativeColorPressed": "#9ca4bc", + "secondaryAlternativeColorDisabled": "#575f763f", + + "secondaryColorForeground": "#282b33", + "secondaryColorForegroundHovered": "#282b33", + "secondaryColorForegroundPressed": "#282b33", + "secondaryColorForegroundDisabled": "#282b333f", + + "semiTransparentColor1": "#b7c9ff18", + "semiTransparentColor2": "#b7c9ff23", + "semiTransparentColor3": "#b7c9ff28", + "semiTransparentColor4": "#b7c9ff2d", + + "shadowColor1": "#00000066", + "shadowColor2": "#000000bb", + "shadowColor3": "#000000ff", + + "statusColorForeground": "#ffffff", + "statusColorForegroundHovered": "#ffffff", + "statusColorForegroundPressed": "#ffffff", + "statusColorForegroundDisabled": "#ffffff26", + + "statusColorError": "#e96b72", + "statusColorErrorHovered": "#f47c83", + "statusColorErrorPressed": "#ff9197", + "statusColorErrorDisabled": "#3f333b", + + "statusColorInfo": "#1ba8d5", + "statusColorInfoHovered": "#1eb5e5", + "statusColorInfoPressed": "#29c0f0", + "statusColorInfoDisabled": "#283345", + + "statusColorSuccess": "#2bb5a0", + "statusColorSuccessHovered": "#3cbfab", + "statusColorSuccessPressed": "#4ecdb9", + "statusColorSuccessDisabled": "#28363c", + + "statusColorWarning": "#fbc064", + "statusColorWarningHovered": "#ffcf6c", + "statusColorWarningPressed": "#ffd880", + "statusColorWarningDisabled": "#393737" +} diff --git a/sandbox/resources/light.json b/showcase/resources/themes/light.json similarity index 87% rename from sandbox/resources/light.json rename to showcase/resources/themes/light.json index 2c7c841..963e6a6 100644 --- a/sandbox/resources/light.json +++ b/showcase/resources/themes/light.json @@ -1,71 +1,91 @@ { + "meta": { + "author": "Olivier Cléro", + "name": "Light", + "version": "1.5.0" + }, + "backgroundColorMain1": "#ffffff", "backgroundColorMain2": "#f3f3f3", "backgroundColorMain3": "#e3e3e3", - "backgroundColorMain4": "#dcdcdc", + "backgroundColorMain4": "#dfdfdf", + + "backgroundColorWorkspace": "#b7b7b7", + "backgroundColorTabBar": "#dfdfdf", + "borderColor": "#d3d3d3", "borderColorDisabled": "#e9e9e9", "borderColorHovered": "#b3b3b3", "borderColorPressed": "#a3a3a3", + "focusColor": "#40a9ff66", - "meta": { - "author": "Olivier Cléro", - "name": "Light", - "version": "1.4.0" - }, - "neutralColor": "#e1e1e1", + + "neutralColor": "#d1d1d1", + "neutralColorHovered": "#d3d3d3", + "neutralColorPressed": "#d5d5d5", "neutralColorDisabled": "#eeeeee", - "neutralColorHovered": "#d9d9d9", - "neutralColorPressed": "#d2d2d2", + + "primaryColor": "#1890ff", + "primaryColorHovered": "#2c9dff", + "primaryColorPressed": "#40a9ff", + "primaryColorDisabled": "#d1e9ff", + "primaryAlternativeColor": "#106ef9", "primaryAlternativeColorDisabled": "#a9d6ff", "primaryAlternativeColorHovered": "#107bfd", "primaryAlternativeColorPressed": "#108bfd", - "primaryColor": "#1890ff", - "primaryColorDisabled": "#d1e9ff", + "primaryColorForeground": "#ffffff", "primaryColorForegroundDisabled": "#ecf6ff", "primaryColorForegroundHovered": "#ffffff", "primaryColorForegroundPressed": "#ffffff", - "primaryColorHovered": "#2c9dff", - "primaryColorPressed": "#40a9ff", + + "secondaryColor": "#404040", + "secondaryColorHovered": "#333333", + "secondaryColorPressed": "#262626", + "secondaryColorDisabled": "#d4d4d4", + "secondaryAlternativeColor": "#909090", "secondaryAlternativeColorDisabled": "#c3c3c3", "secondaryAlternativeColorHovered": "#747474", "secondaryAlternativeColorPressed": "#828282", - "secondaryColor": "#404040", - "secondaryColorDisabled": "#d4d4d4", + "secondaryColorForeground": "#ffffff", "secondaryColorForegroundDisabled": "#ededed", "secondaryColorForegroundHovered": "#ffffff", "secondaryColorForegroundPressed": "#ffffff", - "secondaryColorHovered": "#333333", - "secondaryColorPressed": "#262626", + "semiTransparentColor1": "#0000000a", "semiTransparentColor2": "#00000019", "semiTransparentColor3": "#00000021", "semiTransparentColor4": "#00000028", + "shadowColor1": "#00000020", "shadowColor2": "#00000040", "shadowColor3": "#00000060", + "statusColorError": "#e96b72", - "statusColorErrorDisabled": "#f9dadc", "statusColorErrorHovered": "#f47c83", "statusColorErrorPressed": "#ff9197", + "statusColorErrorDisabled": "#f9dadc", + "statusColorForeground": "#ffffff", - "statusColorForegroundDisabled": "#ffffff99", "statusColorForegroundHovered": "#ffffff", "statusColorForegroundPressed": "#ffffff", + "statusColorForegroundDisabled": "#ffffff99", + "statusColorInfo": "#1ba8d5", - "statusColorInfoDisabled": "#c7eaf5", "statusColorInfoHovered": "#1eb5e5", "statusColorInfoPressed": "#29c0f0", + "statusColorInfoDisabled": "#c7eaf5", + "statusColorSuccess": "#2bb5a0", - "statusColorSuccessDisabled": "#d5f0ec", "statusColorSuccessHovered": "#3cbfab", "statusColorSuccessPressed": "#4ecdb9", + "statusColorSuccessDisabled": "#d5f0ec", + "statusColorWarning": "#fbc064", - "statusColorWarningDisabled": "#feefd8", "statusColorWarningHovered": "#ffcf6c", - "statusColorWarningPressed": "#ffd880" + "statusColorWarningPressed": "#ffd880", + "statusColorWarningDisabled": "#feefd8" } diff --git a/showcase/src/ShowcaseWindow.cpp b/showcase/src/ShowcaseWindow.cpp new file mode 100644 index 0000000..6ceb9e8 --- /dev/null +++ b/showcase/src/ShowcaseWindow.cpp @@ -0,0 +1,746 @@ +// SPDX-FileCopyrightText: Olivier Cléro +// SPDX-License-Identifier: MIT + +#include "ShowcaseWindow.hpp" + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace oclero::qlementine::showcase { +using Icons16 = oclero::qlementine::icons::Icons16; + +static QIcon makeThemedIcon(Icons16 id, const QSize& size = { 16, 16 }) { + const auto svgPath = oclero::qlementine::icons::iconPath(id); + if (auto* style = oclero::qlementine::appStyle()) { + return style->makeThemedIcon(svgPath, size); + } else { + return QIcon(svgPath); + } +} + +class DummyWorkspace : public QWidget { +public: + using QWidget::QWidget; + +protected: + void paintEvent(QPaintEvent* evt) override { + QPainter p(this); + + QColor backgroundColor; + if (const auto* qlementine_style = qlementine::appStyle()) { + const auto theme = qlementine_style->theme(); + backgroundColor = theme.backgroundColorWorkspace; + } + p.fillRect(rect(), backgroundColor); + } +}; + +static QString getDummyText(const unsigned int minWords = 3, const unsigned int maxWords = 4) { + static const auto loremIpsumWords = std::array{ "Lorem", "Ipsum", "Dolor", "Sit", "Amet", + "Consectetur", "Adipiscing", "Elit", "Sed", "Do", "Eiusmod", "Tempor", "Incididunt", "Ut", "Labore", "Et", "Dolore", + "Magna", "Aliqua", "Ut", "Enim", "Ad", "Minim", "Veniam", "Quis", "Nostrud", "Exercitation", "Ullamco", "Laboris", + "Nisi", "Ut", "Aliquip", "Ex", "Ea", "Commodo", "Consequat", "Duis", "Aute", "Irure", "Dolor", "In", + "Reprehenderit", "In", "Voluptate", "Velit", "Esse", "Cillum", "Dolore", "Eu", "Fugiat", "Nulla", "Pariatur", + "Excepteur", "Sint", "Occaecat", "Cupidatat", "Non", "Proident", "Sunt", "In", "Culpa", "Qui", "Officia", + "Deserunt", "Mollit", "Anim", "Id", "Est", "Laborum" }; + + auto rd = std::random_device(); + auto gen = std::mt19937(rd()); + + auto randomCountDistrib = std::uniform_int_distribution<>(minWords, maxWords); + const auto random_word_count = randomCountDistrib(gen); + + auto randomIndexDistrib = std::uniform_int_distribution<>(0, loremIpsumWords.size() - 1 - random_word_count); + const auto randomWordIndex = randomIndexDistrib(gen); + + auto result = loremIpsumWords.at(randomWordIndex); + for (auto i = 0; i < random_word_count - 1; ++i) { + result += ' ' + loremIpsumWords.at(randomWordIndex + 1 + i); + } + return result; +} + +static QIcon getDummyColoredIcon() { + static const auto icons = std::array{ + QIcon(":/showcase/icons/cube-green.svg"), + QIcon(":/showcase/icons/cube-red.svg"), + QIcon(":/showcase/icons/cube-yellow.svg"), + }; + + auto rd = std::random_device(); + auto gen = std::mt19937(rd()); + + auto randomDistrib = std::uniform_int_distribution<>(0, icons.size() - 1); + const auto randomIndex = randomDistrib(gen); + + return icons.at(randomIndex); +} + +static QIcon getDummyMonochromeIcon(const QSize& size = { 16, 16 }) { + auto rd = std::random_device(); + auto gen = std::mt19937(rd()); + + auto randomDistrib = + std::uniform_int_distribution>(1, 410 - 1); // TODO use a constexpr variable. + const auto randomIndex = randomDistrib(gen); + const auto randomIcon = static_cast(randomIndex); + return makeThemedIcon(randomIcon, size); +} + +struct ShowcaseWindow::Impl { + ShowcaseWindow& owner; + QPointer qlementineStyle; + QPointer themeManager; + QVBoxLayout* rootLayout{ nullptr }; + QMenuBar* menuBar{ nullptr }; + QTabBar* tabBar{ nullptr }; + QToolBar* toolBar{ nullptr }; + QSplitter* splitter{ nullptr }; + QWidget* leftPanel{ nullptr }; + QWidget* rightPanel{ nullptr }; + QWidget* workspace{ nullptr }; + QStatusBar* statusBar{ nullptr }; + oclero::qlementine::Switch* themeSwitch{ nullptr }; + + Impl(ShowcaseWindow& o, ThemeManager* themeManager) + : owner(o) + , themeManager(themeManager) {} + + void setupUI() { + setupMenuBar(); + setupTabBar(); + setupToolBar(); + setupLeftPanel(); + setupRightPanel(); + setupWorkspace(); + setupSplitter(); + setupStatusBar(); + setupLayout(); + } + + void setTheme(const QString& theme) { + if (themeManager) { + themeManager->setCurrentTheme(theme); + } + } + + void switchTheme() { + if (themeManager) { + themeManager->setNextTheme(); + } + } + + void updateThemeSwitch() { + themeSwitch->blockSignals(true); + themeSwitch->setChecked(themeManager ? themeManager->currentTheme() == "Dark" : false); + themeSwitch->blockSignals(false); + } + + void setupMenuBar() { + menuBar = new QMenuBar(nullptr); + const auto cb = []() {}; + + { + auto* menu = menuBar->addMenu("File"); + { + // TODO: Use the enum provided by Qt6 instead of strings for icon IDs. + menu->addAction(makeThemedIcon(Icons16::Document_New), "New", QKeySequence::StandardKey::New, cb); + menu->addAction(makeThemedIcon(Icons16::Document_Open), "Open...", QKeySequence::StandardKey::Open, cb); + + auto* recentFilesMenu = menu->addMenu(makeThemedIcon(Icons16::Document_OpenRecent), "Recent Files"); + for (auto i = 0; i < 5; ++i) { + recentFilesMenu->addAction( + makeThemedIcon(Icons16::File_File), QString("Recent File %1").arg(i + 1), QKeySequence{}, cb); + } + + menu->addSeparator(); + menu->addAction(makeThemedIcon(Icons16::Action_Save), "Save", QKeySequence::StandardKey::Save, cb); + menu->addAction(makeThemedIcon(Icons16::Action_Close), "Close", QKeySequence::StandardKey::Close, cb); + menu->addAction(makeThemedIcon(Icons16::Action_Print), "Print...", QKeySequence::StandardKey::Print, cb); + menu->addAction(makeThemedIcon(Icons16::Action_PrintPreview), "Print Preview...", QKeySequence{}, cb); + + menu->addSeparator(); + menu->addAction( + makeThemedIcon(Icons16::Navigation_Settings), "Preferences...", QKeySequence::StandardKey::Preferences, cb); + + menu->addSeparator(); +#ifdef Q_OS_WIN + // QKeySequence::Quit is empty on Windows. + const auto quitShortcut = QKeySequence(Qt::CTRL | Qt::Key_Q); +#else + const auto quitShortcut = QKeySequence(QKeySequence::Quit); +#endif + menu->addAction(makeThemedIcon(Icons16::Action_Close), "Quit", quitShortcut, []() { + qApp->quit(); + }); + } + } + { + auto* menu = menuBar->addMenu("Edit"); + { + menu->addAction(makeThemedIcon(Icons16::Action_Undo), "Undo", QKeySequence::StandardKey::Undo, cb); + menu->addAction(makeThemedIcon(Icons16::Action_Redo), "Redo", QKeySequence::StandardKey::Redo, cb); + + menu->addSeparator(); + menu->addAction(makeThemedIcon(Icons16::Action_Cut), "Cut", QKeySequence::StandardKey::Cut, cb); + menu->addAction(makeThemedIcon(Icons16::Action_Copy), "Copy", QKeySequence::StandardKey::Copy, cb); + menu->addAction(makeThemedIcon(Icons16::Action_Paste), "Paste", QKeySequence::StandardKey::Paste, cb); + menu->addAction(makeThemedIcon(Icons16::Action_Trash), "Delete", QKeySequence::StandardKey::Delete, cb); + } + } + { + auto* menu = menuBar->addMenu("View"); + { + menu->addAction(makeThemedIcon(Icons16::Action_ZoomIn), "Zoom In", QKeySequence::StandardKey::ZoomIn, cb); + menu->addAction(makeThemedIcon(Icons16::Action_ZoomOut), "Zoom Out", QKeySequence::StandardKey::ZoomOut, cb); + menu->addAction(makeThemedIcon(Icons16::Action_ZoomFit), "Fit", QKeySequence{}, cb); + + menu->addSeparator(); + menu->addAction( + makeThemedIcon(Icons16::Action_Fullscreen), "Full Screen", QKeySequence::StandardKey::FullScreen, cb); + + if (themeManager) { + auto* themeMenu = menu->addMenu("Theme"); + themeMenu->setIcon(makeThemedIcon(Icons16::Misc_PaintPalette)); + + auto* themeActionGroup = new QActionGroup(themeMenu); + themeActionGroup->setExclusive(true); + + const auto& themes = themeManager->themes(); + const auto currentTheme = themeManager->currentTheme(); + + for (const auto& theme : themes) { + const auto name = theme.meta.name; + const auto icon = name == "Dark" ? makeThemedIcon(Icons16::Misc_Moon) : makeThemedIcon(Icons16::Misc_Sun); + auto* action = themeMenu->addAction(icon, name); + action->setCheckable(true); + themeActionGroup->addAction(action); + action->setChecked(name == currentTheme); + + QObject::connect(action, &QAction::triggered, action, [this, name](auto checked) { + if (checked) { + setTheme(name); + } + }); + QObject::connect( + themeManager, &oclero::qlementine::ThemeManager::currentThemeChanged, action, [this, name, action]() { + action->setChecked(name == themeManager->currentTheme()); + }); + } + + themeMenu->addSeparator(); + themeMenu->addAction( + makeThemedIcon(Icons16::Action_Swap), "Switch Theme", { Qt::CTRL | Qt::Key_T }, [this]() { + switchTheme(); + }); + } + } + } + { + auto* menu = menuBar->addMenu("Help"); + { + menu->addAction(makeThemedIcon(Icons16::Misc_Mail), "Contact", QKeySequence{}, cb); + menu->addAction(makeThemedIcon(Icons16::Misc_Info), "About...", QKeySequence{}, cb); + } + } + } + + void setupTabBar() { + tabBar = new QTabBar(&owner); + tabBar->setDocumentMode(true); + tabBar->setFocusPolicy(Qt::NoFocus); + tabBar->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + tabBar->setTabsClosable(true); + tabBar->setMovable(false); + tabBar->setExpanding(false); + tabBar->setChangeCurrentOnDrag(true); + tabBar->setUsesScrollButtons(true); + + qlementineStyle->setAutoIconColor(tabBar, oclero::qlementine::AutoIconColor::ForegroundColor); + + for (auto i = 0; i < 4; ++i) { + tabBar->addTab(makeThemedIcon(Icons16::File_File), getDummyText()); + } + + QObject::connect(tabBar, &QTabBar::tabCloseRequested, tabBar, [this](int index) { + tabBar->removeTab(index); + }); + } + + void setupToolBar() { + toolBar = new QToolBar("App ToolBar", &owner); + toolBar->setBackgroundRole(QPalette::ColorRole::Window); + toolBar->setAutoFillBackground(false); + toolBar->setAllowedAreas(Qt::ToolBarArea::TopToolBarArea); + toolBar->setMovable(false); + toolBar->setFloatable(false); + toolBar->setToolButtonStyle(Qt::ToolButtonStyle::ToolButtonFollowStyle); + + const auto addButton = [this](const Icons16 icon, const QString& tooltip, const QString& text = {}) { + auto* toolButton = new QToolButton(toolBar); + toolButton->setFocusPolicy(Qt::NoFocus); + toolButton->setIcon(makeThemedIcon(icon)); + toolButton->setToolTip(tooltip); + if (!text.isEmpty()) { + toolButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toolButton->setText(text); + } + toolBar->addWidget(toolButton); + return toolButton; + }; + + addButton(Icons16::Action_Save, "Save"); + addButton(Icons16::Action_Print, "Print"); + toolBar->addSeparator(); + addButton(Icons16::Action_Undo, "Undo"); + addButton(Icons16::Action_Redo, "Redo"); + + auto* resetButton = addButton(Icons16::Action_Reset, "Reset"); + { + auto* menu = new QMenu(resetButton); + for (auto i = 0; i < 10; ++i) { + menu->addAction(new QAction(getDummyMonochromeIcon(), getDummyText(2, 3), menu)); + } + resetButton->setMenu(menu); + resetButton->setPopupMode(QToolButton::ToolButtonPopupMode::MenuButtonPopup); + } + + toolBar->addSeparator(); + addButton(Icons16::Action_Copy, "Copy"); + addButton(Icons16::Action_Paste, "Paste"); + addButton(Icons16::Action_Cut, "Cut"); + toolBar->addSeparator(); + addButton(Icons16::Media_SkipBackward, "Skip Backward"); + addButton(Icons16::Media_Play, "Play"); + addButton(Icons16::Media_SkipForward, "Skip Forward"); + toolBar->addSeparator(); + auto* exportButton = addButton(Icons16::Action_Export, "Export", "Export"); + { + auto* menu = new QMenu(exportButton); + menu->addAction(new QAction(makeThemedIcon(Icons16::File_Movie), "Movie", menu)); + menu->addAction(new QAction(makeThemedIcon(Icons16::File_Picture), "Picture", menu)); + menu->addSeparator(); + menu->addAction(new QAction(makeThemedIcon(Icons16::File_Archive), "Archive", menu)); + + exportButton->setMenu(menu); + exportButton->setPopupMode(QToolButton::ToolButtonPopupMode::MenuButtonPopup); + } + + // Spacer. + auto* spacer_widget = new QWidget(toolBar); + spacer_widget->setAttribute(Qt::WA_TransparentForMouseEvents); + spacer_widget->setMinimumSize(0, 0); + spacer_widget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Ignored); + spacer_widget->setUpdatesEnabled(false); // No paint events. + toolBar->addWidget(spacer_widget); + + // Theme switch. + auto* themeWidget = new QWidget(toolBar); + { + const auto hSpacing = getLayoutHSpacing(themeWidget) / 2; + themeWidget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + auto* themeLayout = new QHBoxLayout(themeWidget); + themeLayout->setSpacing(hSpacing); + themeLayout->setContentsMargins(0, 0, 0, 0); + themeWidget->setLayout(themeLayout); + + auto* lightIconWidget = new oclero::qlementine::IconWidget(makeThemedIcon(Icons16::Misc_Sun), themeWidget); + themeLayout->addWidget(lightIconWidget); + + themeSwitch = new oclero::qlementine::Switch(toolBar); + themeSwitch->setToolTip("Switch between light and dark theme"); + QObject::connect(themeSwitch, &oclero::qlementine::Switch::clicked, themeSwitch, [this](auto checked) { + setTheme(checked ? "Dark" : "Light"); + }); + QObject::connect(themeManager, &oclero::qlementine::ThemeManager::currentThemeChanged, themeSwitch, [this]() { + updateThemeSwitch(); + }); + themeLayout->addWidget(themeSwitch); + + auto* darkIconWidget = new oclero::qlementine::IconWidget(makeThemedIcon(Icons16::Misc_Moon), themeWidget); + themeLayout->addWidget(darkIconWidget); + + updateThemeSwitch(); + } + toolBar->addWidget(themeWidget); + } + + void setupLeftPanel() { + auto* widget = new QWidget(&owner); + leftPanel = widget; + leftPanel->setMinimumWidth(200); + leftPanel->setMaximumWidth(400); + + auto* layout = new QVBoxLayout(widget); + layout->setContentsMargins({ 0, 0, 0, 0 }); + layout->setSpacing(0); + + { + auto* topBar = new QWidget(widget); + layout->addWidget(topBar); + auto* topBarLayout = new QHBoxLayout(topBar); + topBarLayout->setContentsMargins({ 12, 8, 12, 8 }); + + auto* lineEdit = new LineEdit(widget); + lineEdit->setIcon(makeThemedIcon(Icons16::Navigation_Search)); + lineEdit->setClearButtonEnabled(true); + lineEdit->setPlaceholderText("Search..."); + topBarLayout->addWidget(lineEdit, 1); + + auto* button = new QPushButton(makeThemedIcon(Icons16::Action_Filter), "", widget); + button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + topBarLayout->addWidget(button); + } + + auto* navBar = new NavigationBar(widget); + { + layout->addWidget(navBar); + navBar->setItemsShouldExpand(true); + navBar->addItem("Objects", QIcon(), QString("%1").arg(12)); + navBar->addItem("Materials", QIcon(), QString("%1").arg(3)); + } + + layout->addWidget(makeHorizontalLine(widget)); + + auto* stackedWidget = new QStackedWidget(widget); + { + stackedWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); + + { + auto* treeWidget = new QTreeWidget(widget); + stackedWidget->addWidget(treeWidget); + + qlementineStyle->setAutoIconColor(treeWidget, oclero::qlementine::AutoIconColor::None); + + treeWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); + treeWidget->setAlternatingRowColors(false); + treeWidget->setColumnCount(1); + treeWidget->setHeaderHidden(true); + treeWidget->setSelectionBehavior(QAbstractItemView::SelectRows); + + for (auto i = 0; i < 24; ++i) { + auto* root = new QTreeWidgetItem(treeWidget); + root->setText(0, getDummyText()); + root->setIcon(0, getDummyColoredIcon()); + + for (auto j = 0; j < 4; ++j) { + auto* child = new QTreeWidgetItem(root); + child->setText(0, getDummyText()); + child->setIcon(0, getDummyColoredIcon()); + + for (auto k = 0; k < 3; ++k) { + auto* subChild = new QTreeWidgetItem(child); + subChild->setText(0, getDummyText()); + subChild->setIcon(0, getDummyColoredIcon()); + } + } + } + + treeWidget->topLevelItem(0)->setSelected(true); + navBar->setItemBadge(0, QString::number(treeWidget->topLevelItemCount())); + } + + { + class CustomDelegate : public QStyledItemDelegate { + using QStyledItemDelegate::QStyledItemDelegate; + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override { + const auto result = QStyledItemDelegate::sizeHint(option, index); + return { 0, result.height() }; + } + }; + + + auto* listWidget = new QListWidget(widget); + stackedWidget->addWidget(listWidget); + + listWidget->setItemDelegate(new CustomDelegate(listWidget)); + listWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); + listWidget->setSizeAdjustPolicy(QListView::SizeAdjustPolicy::AdjustIgnored); + listWidget->setAlternatingRowColors(true); + listWidget->setIconSize(QSize(32, 32)); + qlementineStyle->setAutoIconColor(listWidget, AutoIconColor::None); + + for (auto i = 0; i < 15; ++i) { + const auto itemText = QString("Item #%1 with very long text that can be elided").arg(i); + auto* item = new QListWidgetItem(getDummyColoredIcon(), itemText, listWidget); + item->setFlags(item->flags() | Qt::ItemFlag::ItemIsUserCheckable); + item->setCheckState(i % 3 == 0 ? Qt ::CheckState::Checked : Qt::CheckState::Unchecked); + listWidget->addItem(item); + } + listWidget->item(0)->setSelected(true); + navBar->setItemBadge(1, QString::number(listWidget->count())); + } + + layout->addWidget(stackedWidget, 1); + } + + { + stackedWidget->setCurrentIndex(navBar->currentIndex()); + QObject::connect(navBar, &NavigationBar::currentIndexChanged, stackedWidget, [stackedWidget, navBar]() { + stackedWidget->setCurrentIndex(navBar->currentIndex()); + }); + } + } + + void setupRightPanel() { + auto* widget = new QWidget(&owner); + rightPanel = widget; + rightPanel->setMinimumWidth(200); + rightPanel->setMaximumWidth(400); + + auto* layout = new QVBoxLayout(widget); + layout->setContentsMargins({ 0, 0, 0, 0 }); + layout->setSpacing(0); + + { + auto* topBar = new QWidget(widget); + layout->addWidget(topBar); + auto* topBarLayout = new QHBoxLayout(topBar); + topBarLayout->setContentsMargins({ 12, 8, 12, 8 }); + + { + auto* segmentedControl = new SegmentedControl(topBar); + topBarLayout->addWidget(segmentedControl); + segmentedControl->setItemsShouldExpand(false); + segmentedControl->addItem( + "Properties", makeThemedIcon(Icons16::Navigation_SlidersVertical), QString("%1").arg(4)); + segmentedControl->addItem("Scene", makeThemedIcon(Icons16::Misc_Globe), QString("%1").arg(2)); + } + } + + layout->addWidget(makeHorizontalLine(widget)); + + { + auto* scrollArea = new QScrollArea(widget); + layout->addWidget(scrollArea); + + auto* content = new QWidget(scrollArea); + content->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::MinimumExpanding); + scrollArea->setWidget(content); + scrollArea->setWidgetResizable(true); + + const auto margins = getLayoutMargins(content); + const auto vMargin = static_cast(margins.top() * .75); + auto* contentLayout = new QFormLayout(content); + contentLayout->setFieldGrowthPolicy(QFormLayout::FieldGrowthPolicy::ExpandingFieldsGrow); + contentLayout->setContentsMargins({ margins.left(), vMargin, margins.right(), vMargin }); + { + { + auto* groupBox = new QGroupBox(content); + groupBox->setAlignment(Qt::AlignRight); + groupBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + groupBox->setTitle(getDummyText()); + groupBox->setCheckable(true); + groupBox->setFlat(false); + auto* groupBoxLayout = new QFormLayout(groupBox); + groupBoxLayout->setFieldGrowthPolicy(QFormLayout::FieldGrowthPolicy::ExpandingFieldsGrow); + + { + auto* switchWidget = new qlementine::Switch(groupBox); + switchWidget->setChecked(true); + groupBoxLayout->addRow(getDummyText(2, 2) + ":", switchWidget); + } + { + auto* spinBox = new QSpinBox(groupBox); + spinBox->setRange(0, 1000); + spinBox->setSuffix("cm"); + groupBoxLayout->addRow(getDummyText(2, 2) + ":", spinBox); + } + { + auto* comboBox = new QComboBox(groupBox); + for (auto i = 0; i < 5; ++i) { + comboBox->addItem(getDummyMonochromeIcon(), getDummyText(1, 1)); + } + comboBox->setCurrentIndex(0); + groupBoxLayout->addRow(getDummyText(1, 1) + ":", comboBox); + } + { + auto* comboBox = new QComboBox(groupBox); + comboBox->setEditable(true); + comboBox->setPlaceholderText("Placeholder"); + comboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + for (auto i = 0; i < 5; ++i) { + comboBox->addItem(getDummyMonochromeIcon(), getDummyText(1, 1)); + } + comboBox->setCurrentIndex(0); + groupBoxLayout->addRow(getDummyText(1, 1) + ":", comboBox); + } + { + auto* dateTimeEdit = new QDateTimeEdit(groupBox); + //dateTimeEdit->setSizePolicy(QSizepol) + dateTimeEdit->setCalendarPopup(true); + groupBoxLayout->addRow(getDummyText(1, 1) + ":", dateTimeEdit); + } + { + auto* lineEdit = new QLineEdit(groupBox); + lineEdit->setPlaceholderText("Enter text..."); + lineEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + lineEdit->setClearButtonEnabled(true); + groupBoxLayout->addRow(getDummyText(1, 1) + ":", lineEdit); + } + contentLayout->addRow(groupBox); + } + { + auto* groupBox = new QGroupBox(content); + groupBox->setAlignment(Qt::AlignRight); + groupBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + groupBox->setTitle(getDummyText()); + groupBox->setCheckable(true); + groupBox->setFlat(false); + auto* groupBoxLayout = new QVBoxLayout(groupBox); + { + auto* radioGroup = new QButtonGroup(groupBox); + for (auto i = 0; i < 3; ++i) { + auto* radioButton = new QRadioButton(getDummyText(), groupBox); + radioButton->setChecked(i == 0); + radioButton->setIcon(getDummyMonochromeIcon()); + radioButton->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + radioGroup->addButton(radioButton); + groupBoxLayout->addWidget(radioButton); + } + } + contentLayout->addRow(groupBox); + } + { + auto* groupBox = new QGroupBox(content); + groupBox->setAlignment(Qt::AlignRight); + groupBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + groupBox->setTitle(getDummyText()); + groupBox->setCheckable(true); + groupBox->setFlat(false); + auto* groupBoxLayout = new QFormLayout(groupBox); + { + auto* slider = new QSlider(groupBox); + slider->setRange(0, 100); + slider->setValue(30); + groupBoxLayout->addRow(getDummyText(1, 1) + ":", slider); + } + { + auto* slider = new QSlider(groupBox); + slider->setOrientation(Qt::Orientation::Horizontal); + slider->setRange(0, 10); + slider->setPageStep(1); + slider->setSingleStep(1); + slider->setValue(7); + slider->setTickPosition(QSlider::TickPosition::TicksAbove); + slider->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + + groupBoxLayout->addRow(getDummyText(1, 1) + ":", slider); + } + contentLayout->addRow(groupBox); + } + } + } + } + + void setupWorkspace() { + workspace = new DummyWorkspace(&owner); + workspace->setFocusPolicy(Qt::StrongFocus); + workspace->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + } + + void setupSplitter() { + splitter = new QSplitter(&owner); + splitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + splitter->setOrientation(Qt::Horizontal); + splitter->addWidget(leftPanel); + splitter->addWidget(workspace); + splitter->addWidget(rightPanel); + + splitter->setStretchFactor(0, 2); + splitter->setStretchFactor(1, 6); + splitter->setStretchFactor(2, 2); + } + + void setupStatusBar() { + statusBar = new QStatusBar(&owner); + statusBar->setSizeGripEnabled(false); + + const auto margins = getLayoutMargins(statusBar); + statusBar->setContentsMargins(margins.left(), 0, margins.right(), 0); + { + auto* progressBar = new QProgressBar(statusBar); + progressBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + progressBar->setTextVisible(false); + progressBar->setRange(0, 0); + statusBar->addPermanentWidget(progressBar); + } + } + + void setupLayout() { + rootLayout = new QVBoxLayout(&owner); + rootLayout->setContentsMargins(0, 0, 0, 0); + rootLayout->setSpacing(0); + rootLayout->setMenuBar(menuBar); + rootLayout->addWidget(tabBar); + rootLayout->addWidget(toolBar); + rootLayout->addWidget(splitter); + rootLayout->addWidget(statusBar); + workspace->setFocus(Qt::NoFocusReason); + } +}; + +ShowcaseWindow::ShowcaseWindow(ThemeManager* themeManager, QWidget* parent) + : QWidget(parent) + , _impl(new Impl(*this, themeManager)) { + setWindowIcon(QIcon(QStringLiteral(":/showcase/qlementine_icon.ico"))); + _impl->setupUI(); + setMinimumSize(600, 400); + resize(800, 600); + oclero::qlementine::centerWidget(this); + + this->ensurePolished(); + _impl->qlementineStyle = qobject_cast(this->style()); +} + +ShowcaseWindow::~ShowcaseWindow() = default; +} // namespace oclero::qlementine::showcase diff --git a/showcase/src/ShowcaseWindow.hpp b/showcase/src/ShowcaseWindow.hpp new file mode 100644 index 0000000..6c93088 --- /dev/null +++ b/showcase/src/ShowcaseWindow.hpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Olivier Cléro +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include + +namespace oclero::qlementine { +class QlementineStyle; +class ThemeManager; +} // namespace oclero::qlementine + +namespace oclero::qlementine::showcase { +class ShowcaseWindow : public QWidget { +public: + ShowcaseWindow(ThemeManager* themeManager = nullptr, QWidget* parent = nullptr); + ~ShowcaseWindow(); + +private: + struct Impl; + std::unique_ptr _impl{}; +}; +} // namespace oclero::qlementine::showcase diff --git a/showcase/src/main.cpp b/showcase/src/main.cpp new file mode 100644 index 0000000..4cd3521 --- /dev/null +++ b/showcase/src/main.cpp @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Olivier Cléro +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include + +#include "ShowcaseWindow.hpp" + +#define USE_CUSTOM_STYLE 1 + +int main(int argc, char* argv[]) { + // Must be set before creating a QApplication. + QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + + QApplication qApplication(argc, argv); + + // Must be set after creating a QApplication. + QGuiApplication::setApplicationDisplayName("Showcase"); + QCoreApplication::setApplicationName("Showcase"); + QGuiApplication::setDesktopFileName("Showcase"); + QCoreApplication::setOrganizationName("oclero"); + QCoreApplication::setOrganizationDomain("olivierclero.com"); + QCoreApplication::setApplicationVersion("1.0.0"); + QApplication::setWindowIcon(QIcon(QStringLiteral(":/showcase/qlementine_icon.ico"))); + +#if USE_CUSTOM_STYLE + // Custom QStyle. + auto* style = new oclero::qlementine::QlementineStyle(&qApplication); + style->setAnimationsEnabled(true); + style->setAutoIconColor(oclero::qlementine::AutoIconColor::TextColor); + style->setIconPathGetter(oclero::qlementine::icons::fromFreeDesktop); + qApplication.setStyle(style); + + // Custom icon theme. + oclero::qlementine::icons::initializeIconTheme(); + QIcon::setThemeName("qlementine"); + + // Theme manager. + auto* themeManager = new oclero::qlementine::ThemeManager(style); + themeManager->loadDirectory(":/showcase/themes"); + + // Define theme on QStyle. + themeManager->setCurrentTheme("Light"); +#endif + + auto window = std::make_unique(themeManager); + window->show(); + + return qApplication.exec(); +}