Skip to content

change GroupParameterItem palette to address issue in darkmode on mac#2101

Merged
j9ac9k merged 11 commits intopyqtgraph:masterfrom
Wubbzi:paramtreeStyle
Dec 11, 2021
Merged

change GroupParameterItem palette to address issue in darkmode on mac#2101
j9ac9k merged 11 commits intopyqtgraph:masterfrom
Wubbzi:paramtreeStyle

Conversation

@Wubbzi
Copy link
Copy Markdown
Contributor

@Wubbzi Wubbzi commented Nov 15, 2021

This partially addresses #1958

These changes will still need to be tested on windows/Linux.

Group title colors were changed to the color of QPalette.TextRole but it could easily be changed back to the original in dark mode.

Light
unknown-1
Dark
unknown-2

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 15, 2021

A fellow macOS user has entered the chat!

Thanks for tracking this down @Wubbzi

Over the next few days I'll try and test out this change on the other platforms.

@sem-geologist
Copy link
Copy Markdown
Contributor

sem-geologist commented Nov 15, 2021

I had tested this on Linux KDE Neon with breeze (light) and breeze dark themes (runing example with systems python3) Forgot to change branch thus all below images show how it looked before this PR:
Parameter_tree_kde_neon_light

Parameter_tree_kde_neon_breeze_dark

Then also tested with conda (which uses a bit different theminng, more compact, quite similar to KDE system's default breeze, but there are some differences. It is also one of these possibilities to force dark theme on windows using conda's "dark_theme":

everything looks good and readable.
That looked readable, but look below, actually new style looks better!

@sem-geologist
Copy link
Copy Markdown
Contributor

sem-geologist commented Nov 15, 2021

One thing, which is bothering me is why on Your Mac at light theme the font at 0 level gets dark dark, and on my linux KDE neon and conda it is not getting dark at all. It is still readable, but the font color does not kick in, Your code suppose to change that but it does not. I tested if app.property("darkMode") is working, it is. As far as I recall from my other issues (with my software) somehow the font color in my cases could not be directly changed. Changing Font color worked through stylesheets only. Probably it is Qt for linux thing... I am surprised that set color is ignored, but Qt still manages to set color which is enough contrasting.

P.S. thanks to this Issue I got aware that there is this property "darkMode", in my own application I had unnecessary made own function to check for that (checking if fg color is lighter than bg). Good to know Qt has built-in way for finding that out, albeit for some reasons getting QApplication.instance() is not always working in modularized code.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 15, 2021

@sem-geologist macOS does some shenanigans here, and manipulates colors that it thinks the application has specified to be inline with system defaults, to make them appear to match the rest of the system theme; and I believe that's what's happening with some of the transparent backgrounds for some of the widgets (which is why we see labels written twice for example).

Annoyingly, this means you need to be on macOS to replicate the issue.

@Wubbzi thanks for the PR, I'll hop on your branch and do some testing on my machine ...it might take me a day or two.

@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 15, 2021

@sem-geologist Are you sure you're running the code from the PR? The pale lavender color in the outermost groups was part of the code that was replaced. I'd assume that you should only see black or white, seeing as how the text color should be using the TextRole from the application palette.

Setting the text color to the TextRole is redundant, I only defined the color and left the code for setForeground in case someone wanted to play around with using the previous color. If you were to comment out the setForeground line, you shouldn't see a difference, since we're applying the TextRole color to text and the app palette is deciding which color TextRole color is.

The darkMode property is pretty nifty, and its actually part of pg. You can find the code in pyqtgraph/Qt/__init__.py on line 422.

@sem-geologist
Copy link
Copy Markdown
Contributor

sem-geologist commented Nov 15, 2021

oh... I see... I am an idiot - had cloned Your tree, and had not changed the branch 🤦.
Here are the real view, and colors looks same as on Your Mac pictures (I am lazy and not trying conda, just breeze and breeze dark with system python3):
Parameter_tree_kde_neon_light
Parameter_tree_kde_neon_breeze_dark

I am leaving the wrong pictures uploaded above (will comment that are old before this PR), as now I see a very clear difference. These changes preserves readability, but looks much better IMHO, in particularly on the dark theme.

@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 15, 2021

@sem-geologist Thank you for taking the time to check - I was beginning to worry that macOS had gotten the best of me(again).

@ntjess
Copy link
Copy Markdown
Contributor

ntjess commented Nov 16, 2021

@sem-geologist macOS does some shenanigans here, and manipulates colors that it thinks the application has specified to be inline with system defaults, to make them appear to match the rest of the system theme; and I believe that's what's happening with some of the transparent backgrounds for some of the widgets (which is why we see labels written twice for example).

Annoyingly, this means you need to be on macOS to replicate the issue.

@Wubbzi thanks for the PR, I'll hop on your branch and do some testing on my machine ...it might take me a day or two.

This problem also shows on non-mac platforms with really long action names. The label will 'leak' out from underneath the button. I was able to solve this in my application with self.setText(0, '') in the ActionParameterItem constructor, but it would be good to make sure this is also robust against i.e. title changes and so on.

I was trying to find a reference to someone's message in the discord sprint chat but I'm not sure how to search the archived chat

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 16, 2021

I was trying to find a reference to someone's message in the discord sprint chat but I'm not sure how to search the archived chat

I'll ask the mods there if we can get the archive.

@ntjess
Copy link
Copy Markdown
Contributor

ntjess commented Nov 16, 2021

Here was the message I remembered seeing:

So, using a palette didn't seem to work and I'm not sure why. I was able to change the color of the button text, but not the background.
Setting a stylesheet would work, if you want to go that route.
If we change the init in ParameterItem, that also seems to do the trick.

# QtWidgets.QTreeWidgetItem.__init__(self, [param.title(), ''])      
QtWidgets.QTreeWidgetItem.__init__(self, ['', ''])

I think this change would work better just in the override of widgets that don't have a name column, rather than at the ParameterItem level. So I might still recommend the self.setText approach vs. the ParameterItem change, but I'm not sure if it matters much either way

In either case, I think it's fair to say this is outside the scope of the palette PR (unless you want to include it 🙂) (i.e. A transparent button should be fine provided there is no text behind it)

@ntjess
Copy link
Copy Markdown
Contributor

ntjess commented Nov 16, 2021

image

It's not working on qdarkstyle.load_stylesheet (a use case for several users, there's a dedicated pyqtgraph section in the repo. Is that something that can be changed in the palette here, or something qdarkstyle should alter?

image

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 16, 2021

ooooo nice call on checking against qdarkstyle; we may need to get some input from them on how to "best" go about this.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 16, 2021

@ntjess wait... does qdarkstyle render this PR differently from master? If not, there is likely nothing that we can do on our end.

@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 17, 2021

@ntjess @j9ac9k

I didn't even think about qdarkstyle but I added a condition to check for a stylesheet and apply a nicer looking palette in the case one exists.

Screen Shot 63

@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 17, 2021

image

It's not working on qdarkstyle.load_stylesheet (a use case for several users, there's a dedicated pyqtgraph section in the repo. Is that something that can be changed in the palette here, or something qdarkstyle should alter?

image

Which OS/Qt version are you using in the dark screenshot for this PR? I'm not sure why the Example Parameters group is completely white. Did you notice if Save/Restore functionality and the other groups where depth=0 were like this also?

@NilsNemitz
Copy link
Copy Markdown
Contributor

I seem to remember that qdarkstyle just restyles everything by stylesheet, and does not even try to touch the QPalette.
Is there a working way to set/detect dark mode on Windows now?
Would it be useful to force the system into the dark mode palette when qdarkstyle is detected?

@ntjess
Copy link
Copy Markdown
Contributor

ntjess commented Nov 17, 2021

It looks like qdarkstyle indeed is limited to the stylesheet with no palette integrations: https://github.com/ColinDuquesnoy/QDarkStyleSheet/blob/26c3878fff742c535058bf5d08643d0135cde2af/qdarkstyle/__init__.py#L275

@Wubbzi I'm not sure about how to load official dark themes, so I used an unofficial gist here: https://gist.github.com/lschmierer/443b8e21ad93e2a2d7eb

Running on Zorin OS 16 with PyQt5 5.15.2. List of recognized styles just shows "Fusion" and "Windows".

Thus, I'm not confident if my representation is truly indicative of a dark style.

altBackgroundColor = palette.dark().color()

# if stylesheet and 'QDarkStyleSheet' in stylesheet:
if app.styleSheet(): # Assume the stylesheet is qdarkstyle
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking for a stylesheet --I recommend looking for "qdarkstyle" explicitly in the sheet, rather than just checking it is nonzero in length. The header of qdarkstyle's sheet is

/* ---------------------------------------------------------------------------
    WARNING! File created programmatically. All changes made in this file will be lost!
    Created by the qtsass compiler v0.3.0
    The definitions are in the "qdarkstyle.qss._styles.scss" module
--------------------------------------------------------------------------- */
/* Light Style - QDarkStyleSheet ------------------------------------------ */
/*
See Qt documentation:

So there are a few magic strings you can check for

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With load_stylesheet(palette=LightPalette), it looks a bit wonky:
image

Note that "DarkStyle" is commented out in this case as opposed to Light Style

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Hows this? I feel like the text could be better on the outermost groups.. maybe not quite so dark or something with color.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 17, 2021 via email

@NilsNemitz
Copy link
Copy Markdown
Contributor

NilsNemitz commented Nov 17, 2021

Sorry if it isn't helpful, but I've been running a QPalette like this for darkmode on windows. It isn't perfect yet (and the color definitions need some cleanup), but it kind of matches qdarkstyle in colors.

Would it make sense to apply that manually if a desire for dark mode is detected on Windows?

    # QDarkstyle compatible UI colors
    'DS_bg_light' : '#505F69', 'DS_bg_normal' : '#32414B', 'DS_bg_dark' : '#19232D',
    'DS_fg_light' : '#F0F0F0', 'DS_fg_normal' : '#AAAAAA', 'DS_fg_dark' : '#787878',
    'DS_sel_light': '#148CD2', 'DS_sel_normal': '#1464A0', 'DS_sel_dark': '#14506E',

def dark_QPalette():
    # color definitions roughly match QDarkstyle
    BLACK      = QColor('#000000')
    BG_LIGHT   = QColor('#505F69')
    BG_NORMAL  = QColor('#32414B')
    BG_DARK    = QColor('#19232D')
    FG_LIGHT   = QColor('#F0F0F0')
    FG_NORMAL  = QColor('#AAAAAA')
    FG_DARK    = QColor('#787878')
    SEL_LIGHT  = QColor('#148CD2')
    SEL_NORMAL = QColor('#1464A0')
    # SEL_DARK   = QColor('#14506E')
    qpal = QPalette( QColor(BG_DARK) )
    for ptype in (  QPalette.Active,  QPalette.Inactive ):
        qpal.setColor( ptype, QPalette.Window    , QColor( COLORS['DS_bg_normal'] ) )
        qpal.setColor( ptype, QPalette.WindowText, QColor( COLORS['DS_fg_light' ] ) )
        qpal.setColor( ptype, QPalette.Base      , QColor( COLORS['DS_bg_dark'  ] ) )
        qpal.setColor( ptype, QPalette.Text, FG_LIGHT )
        qpal.setColor( ptype, QPalette.AlternateBase, BG_DARK )
        qpal.setColor( ptype, QPalette.ToolTipBase, BG_LIGHT )
        qpal.setColor( ptype, QPalette.ToolTipText, FG_LIGHT )
        qpal.setColor( ptype, QPalette.Button, BG_DARK )
        qpal.setColor( ptype, QPalette.ButtonText, FG_LIGHT )
        qpal.setColor( ptype, QPalette.Link, SEL_NORMAL )
        qpal.setColor( ptype, QPalette.LinkVisited, FG_NORMAL )
        qpal.setColor( ptype, QPalette.Highlight, SEL_LIGHT )
        qpal.setColor( ptype, QPalette.HighlightedText, BLACK )
    qpal.setColor( QPalette.Disabled, QPalette.Button, BG_NORMAL )
    qpal.setColor( QPalette.Disabled, QPalette.ButtonText, FG_DARK )
    qpal.setColor( QPalette.Disabled, QPalette.WindowText, FG_DARK )
    return qpal

@ntjess
Copy link
Copy Markdown
Contributor

ntjess commented Nov 17, 2021

@NilsNemitz Your solution is more robust than that gist -- thanks. Here is the new screenshot, everything appears to be in order

image

@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 17, 2021

Is there a way to extract the color code programmatically from the style sheet that's applied?

We could extract the entire style sheet with app.styleSheet(). Since the styleSheet would be applied to the application and not specific widgets, we would probably need to use regex or manually parse the app's style sheet to pull out the colors for specific widgets.

If we changed the style sheet for any widget we would have to replace everything(margin,border,etc) not just the colors.

I do have some old code that was used to do something very similar that could be adapted if needed.

@NilsNemitz I can't test this right now, but I think we could use the current method for windows although it might need to be tweaked.

def onPaletteChange(palette):
    color = palette.base().color().name()
    app = QtWidgets.QApplication.instance()
    app.setProperty('darkMode', color.lower() != "#ffffff")

@ntjess
Ahh, okay, I guess the palette explains why the colors are a bit off.

Originally i was checking for 'QDarkStyleSheet' at the top of the style sheet but I wasn't sure how many groups one of these trees might have in the real world and I was concerned about slowing down the startup with all of the in checks so I changed it back. I'm happy to change it though.

@NilsNemitz
Copy link
Copy Markdown
Contributor

Does onPaletteChange ever happen on Windows, though?
The current code seems to check explicitly for white. Maybe check for lightness > 0.5 instead? Somebody might set their palette for something a little off-white in the future.

@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 17, 2021

@NilsNemitz

Really glad you asked about this! When I was looking for the answer I realized qApp.paletteChanged is emitted any time mkQApp is used to create or access the running an instance.

This means we need to run onPalletteChanged()(or the code from within the function) before colors are chosen. If we rely on that function running 100% of the time, the apps that create a QApplication() instead of using mkQApp won't have pretty GroupParameterItems.

The darkMode property only seems to be used in this PR, the example app and and the syntax example, so its at least worth testing if Windows does allow changing the base color like this, as it won't break much if there is some edge case where it doesn't work.

@sem-geologist
Copy link
Copy Markdown
Contributor

sem-geologist commented Nov 17, 2021

This (opens pop-corns slowly) is fun to watch :) . Jokes aside, I think most of well thought themes use no absolute white or absolute black. i.e. Breeze Dark (which I won't hide like a lot) of KDE5: https://develop.kde.org/hig/style/color/dark/
look the brightest color (under selection section) called "paper white" for ForegroundActive is still not 6 f's, but a bit a tiny bit toned down white (#fcfcfc), where normal foreground is tiny-winy blued toned-down white #eff0f1.
I like how Qt applications looks on the KDE5. (light breeze looks also good, but I am quite a caveman, and sit with dark themes on).
I have a bit mixed opinion for using .lighten() instead of using OS colors. I wonder how much those are exposed to Qt, I guess for Qt on KDE5 it is alot, where problem could be with other OS. While proposed changes looks good, It seem to force colors a bit out from my used theme (and most of Qt GUI respects that, but this then customizes-out from the rest). I think if those theme colors are not somehow passed to Qt during initiation, and so maybe global Qt settings could be found, it would be nice as third option (or rather it should be checked at very first) to use global Qt theme for foreground, background, altBackground and so on instead of creating the colors on the fly. This PR then goes a bit out from scope and is going to include other OS and Qt theming.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 17, 2021

We could extract the entire style sheet with app.styleSheet(). Since the styleSheet would be applied to the application and not specific widgets, we would probably need to use regex or manually parse the app's style sheet to pull out the colors for specific widgets.

That's probably a bag of pain we want no part of.

I think going w/ hardcoded palette values that @NilsNemitz detailed above for this PR is the way to go. It seems to look good in the most configurations, which is an improvement over the current implementation. That meets the criteria for merging IMO.

I certainly didn't intend for the scope of this to blow up so much, and if we want to revisit proper detection of themes on KDE/Windows/macOS, we can certainly do so, but I don't think this PR is the place to do it.

@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 18, 2021

I think going w/ hardcoded palette values that @NilsNemitz detailed above for this PR is the way to go. It seems to look good in the most configurations, which is an improvement over the current implementation. That meets the criteria for merging IMO.

Sounds like a plan.

I'm thinking that we can check for a qdarkstyle stylesheet(light or dark), if it exists, let the stylesheet take over, if there is no qdarkstyle stylesheet and the darkMode property is set, we apply the palette above to the ParameterTree.

Should I keep the lightMode colors from the initial commit in cases where there is no stylesheet and no darkMode?

I'll wait until tomorrow afternoon to commit the changes in case there are any objections/suggestions.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 18, 2021

I'm thinking that we can check for a qdarkstyle stylesheet(light or dark), if it exists, let the stylesheet take over, if there is no qdarkstyle stylesheet and the darkMode property is set, we apply the palette above to the ParameterTree.

If this check can be done w/o difficulty/complexity; I think that's fine... if users are specifying their own style sheet, they know what they want and would probably serve us to respect that.

Should I keep the lightMode colors from the initial commit in cases where there is no stylesheet and no darkMode?

While those colors definitely look a bit dark for a light mode theme, those colors are definitely intended to be darker than normal, so I personally think they're fine for their intended purpose.

… os color theme is changed while the app is running.
@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 23, 2021

Hi @Wubbzi

I just ran this branch, this worked beautifully 🎊 Handled switch between light/dark mode seamlessly!

I would recommend adopting the suggested changes @NilsNemitz regarding QColor.fromHslF and QColor.getHslF.

Lastly, looks like you're using a python 3.8 feature, cached_property. Unfortunately this is at odds with NEP-29, which has us supporting python 3.7 for all releases until December 26th of this year. So we can certainly keep the cached_property feature, but if we do, we're going to have to hold tight on merging this PR until it's a bit closer to the end of the year.

Looking at the diff, the cached_property has to do w/ the style-sheet detection; in which case I would recommend moving that bit of code into a separate PR that we can merge later.

@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 23, 2021

I would recommend adopting the suggested changes @NilsNemitz regarding QColor.fromHslF and QColor.getHslF.

I've already made these changes to my local branch, I just need to push them to github

Lastly, looks like you're using a python 3.8 feature, cached_property. Unfortunately this is at odds with NEP-29, which has us supporting python 3.7 for all releases until December 26th of this year. So we can certainly keep the cached_property feature, but if we do, we're going to have to hold tight on merging this PR until it's a bit closer to the end of the year.

Unless the tree has 1000s of groups, the difference with and without the caching isn't really noticeable, so I'm fine with moving it out, or changing it to use functools lru cache, if you'd like have some sort of caching until we're able to use cached_property.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 23, 2021

I would recommend adopting the suggested changes @NilsNemitz regarding QColor.fromHslF and QColor.getHslF.

I've already made these changes to my local branch, I just need to push them to github

👍🏻

Lastly, looks like you're using a python 3.8 feature, cached_property. Unfortunately this is at odds with NEP-29, which has us supporting python 3.7 for all releases until December 26th of this year. So we can certainly keep the cached_property feature, but if we do, we're going to have to hold tight on merging this PR until it's a bit closer to the end of the year.

Unless the tree has 1000s of groups, the difference with and without the caching isn't really noticeable, so I'm fine with moving it out, or changing it to use functools lru cache, if you'd like have some sort of caching until we're able to use cached_property.

I'm good w/o caching.

Added method in `pg/__init__.py` to apply app palette based on user input - meant to be used with QDarkStyle
@Wubbzi
Copy link
Copy Markdown
Contributor Author

Wubbzi commented Nov 24, 2021

This update includes a light and dark palette meant to be used with a QDarkStyle stylesheet.

Either the Palette.ID from the QDarkStyle or the style of the palette as a stringlight/dark will work.

pg.applyPalette(app, qdarkstyle.DarkPalette.ID)
app.setStyleSheet(qdarkstyle.load_stylesheet(palette=qdarkstyle.DarkPalette))

pg.applyPalette(app, 'light')
app.setStyleSheet(qdarkstyle.load_stylesheet(palette=qdarkstyle.LightPalette))

return qpal


def light_QPalette():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are keeping these private, should we give them more specific names?
getQDarkStyleDarkQPalette() and getQDarkStyleLightQPalette(), maybe?
I think we might want to keep the namespace open for our own version of just "light" and "dark" palettes.
The very dark colors in QDarkStyle do not work well with e.g. black outlines of UI elements and the black shading applied according to button state. It really needs the stylesheet.
I'd ultimately like to have a dark mode palette that works without depending on an external stylesheet, and I would want to refer to that as just "dark". :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm all for QDarkStyleDarkQPalette() and QDarkStyleLightQPalette() but pep8 doesn't like the java style getters/setter names. We might even want to make these properties.

I'd ultimately like to have a dark mode palette that works without depending on an external stylesheet, and I would want to refer to that as just "dark".

Agree! more about this here

Copy link
Copy Markdown
Contributor

@NilsNemitz NilsNemitz Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the content of palette.py needs some more thought before we let users play with it directly.

This might better use some internal dictionary that is easy to add to. Ideally, the user should be able to throw in their own palette info into the colors folder and grab it from there in multiple applications.

At this time, the strategy might be to keep everything in here private and marked experimental. applyPalette is the only supported access to this... for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the naming:
We are basically following the Qt CamelCase naming conventions, at least on the API side. So we regularly deviate from python standards. @j9ac9k has the best overview here, though.

Copy link
Copy Markdown
Member

@j9ac9k j9ac9k Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Wubbzi we generally deviate from pep8 when it comes to naming convention to public portions of our API as @NilsNemitz suggested. We will go snake_case for methods intended for internal methods, and use the leading underscore to designate if something should be considered private or not.

On top of using camelCase for public API methods; we try and follow Qt naming conventions when feasible (for example we do this with .setData() .getData() and a variety of other methods that we feel are analogous to other parts of the Qt API.

Of course the above are just guidelines. ... regarding applyPalette would it be more appropriate to use .setPalette()? That method is used in other parts of the Qt framework:

EDIT: I looked at the diff and saw you call .setPalette within the method 🤦🏻

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are basically following the Qt CamelCase naming conventions, at least on the API side

I don't have any problem with the case conventions pg uses. The snake_case/true_property stuff that was added in pyside6 looks so wrong, and I hope it never catches on.

I was trying to stick with the Qt conventions. for example, Qt has setMaximumHeight() but when you want to get it, its just maximumHeight() not getMaximumHeight() I stuck with the capital Q because I was afraid it would look weird. Qt does lowercase their methods that begin with q, like qAbs,qRound but you don't see those used in Python very often.

Copy link
Copy Markdown
Contributor Author

@Wubbzi Wubbzi Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@j9ac9k oooh, another dash user!

We will go snake_case for methods intended for internal methods, and use the leading underscore to designate if something should be considered private or not.

Oh, I didn't realize thats what was meant by

the strategy might be to keep everything in here private and marked experimental.

regarding applyPalette would it be more appropriate to use .setPalette()

Yep. I was trying to stick as closely to what Nils suggested as I could, so after I copied it out of chat I never changed it, but it has now been changed.

Copy link
Copy Markdown
Contributor

@NilsNemitz NilsNemitz Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, my thought process wasn't that sophisticated.
More like "make sure nobody uses it directly, whatever we put now, we'll certainly break it later when we put more functionality there"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another dash user!

if you're on macOS and a developer and not using Alfred + Dash ...that's a crime 😆


# when changing from light to dark mode updateDepth(GroupParamItem) will insert what I think is the
# invisible root node. Most of the time, in the example app the title for this node
# will be empty, occasionally its named params.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"it is name 'params'"?
Since the comment is long already, maybe include what actually happens?
"In this case, we hide it again by setting sizeHint to zero."? (Is that what happens?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"it is name 'params'"?

changed it to occasionally the title is 'params'., if that helps to be more clear.

"In this case, we hide it again by setting sizeHint to zero."? (Is that what happens?)

Not exactly, it appears to be hidden by default, unless given a size hint by the code below. I'm making a small change to the code so its easier to tell whats happening.

        title = self.param.title()
        if not title or title == 'params':
            return
        self.setText(0, title)
        fm = QtGui.QFontMetrics(self.font(0))
        textFlags = QtCore.Qt.TextFlag.TextSingleLine
        size = fm.size(textFlags, self.text(0))
        size.setHeight(int(size.height() * 1.35))
        size.setWidth(int(size.width() * 1.15))
        self.setSizeHint(0, size)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just sticking a comment on the setSizeHint line?
# This makes sure that the label remains visible
...or invisible, because I still don't know what this is actually trying to do :)

Copy link
Copy Markdown
Contributor Author

@Wubbzi Wubbzi Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing I've changed here is in this method is:

        # This makes sure that items with no title or the title 'params' remain invisible
        
        title = self.param.title()
        if not title or title == 'params':
            return

If there isn't a title or the title is ''params'` then we don't want to run the code below to give it a size. It seems to default to 0,0 width/height, but I didn't look very deeply into it because simply checking that the title wasn't empty did what we wanted.

How about just sticking a comment on the setSizeHint line?

This isn't my code, but the code on the size hint line sets a size hint of size for column 0. The size is set to the width/height *1.35 of a single line of whichever font self.font(0) returns

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I maybe missed this bit then
--> # This makes sure that items with no title or the title 'params' remain invisible
That was the thing that wasn't clear to me.

Sorry! It just felt weird that there was a three line comment that seemed to build up to something and then didn't tell me what it was actually doing in the end :)

@@ -1,11 +1,10 @@
import builtins
from functools import partial
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still use this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, its being used here in GroupParameterItem and is being used to always return the original font size: The current font size is returned and incremented by 1 for items where depth=0. Since updateDepth is now run every time(to paint the backgrounds), when switching from dark/light system themes, the font size would increase 1 point, every time the theme is switched without it(or something similar).

    def __init__(self, param, depth):
        ParameterItem.__init__(self, param, depth)
        # prevent 0 depth item's font from growing on every call of updateDepth
        self.fontPointSize = partial(self.font(0).pointSize)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it have been the same as self.fontPointSize = self.font(0).pointSize?

Copy link
Copy Markdown
Contributor Author

@Wubbzi Wubbzi Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it have been the same as self.fontPointSize = self.font(0).pointSize?

The behavior would have been the same. The difference to me is, when an attribute is defined as in __init__, generally you wouldn't expect that its required to be called later. Seeing it defined in a lambda, or with partial lets you know that it is a callable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this isn't used any more with the most recent commit; once this import is removed, I think we can merge this 🎉

def __init__(self, param, depth):
ParameterItem.__init__(self, param, depth)
# prevent 0 depth item's font from growing on every call of updateDepth
self.fontPointSize = partial(self.font(0).pointSize)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no experience with this.
If this defines a fontPointSize method for WidgetParameterItem, maybe the boring, verbose way of going through def fontPointSize might be more readable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this specific case, this is basically the same as self.fontPointSize = lambda: self.font(0).pointSize() but is marginally faster.
I didn't define a method because I didn't want to add any methods to the object that weren't necessary elsewhere in the code.

I think this is pretty readable as is but I'll change this into a property instead.

    @ property
    def fontPointSize(self):
        return self.font(0).pointSize()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I find that much easier to read :)
"I will give you this if asked for fontPointSize()"

Last question:
This is not solved by simply overriding / providing a matching pointSize() method here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I follow.. If you're asking "Why does this need to be a property, wouldn't a method work just the same?" the answer is yes, it would work but font.setPointSize(self.fontPointSize() + 1) looks weird compared to font.setPointSize(self.fontPointSize + 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nitpicky and not a large concern. Overall, we typically don't reach into other objects to grab their properties. Even though we are not Java, it seems that we more commonly go through accessor methods.

I think my viewpoint is

  • if this is used internally only (as in your example), then do we even need it over accessing self.font(0).pointSize() directly where applicable?
  • if this provides a regular interface for other parts of the code, going through a normal method definition is the easiest to find and document. And it clearly sells "use this to get this information". This would be particularly true if you are basically reporting the effective pointsize for this ParameterItem. Other objects do that through pointSize(), wouldn't it be nice to match that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is used internally only (as in your example), then do we even need it over accessing self.font(0).pointSize() directly where applicable?

Actually, now that I think about it, the method or lambda won't work either.

# we define the font as
font = self.font(0/1)
# we increment based on the current size
font.setPointSize(self.font(0/1).pointSize() + 1)  # this will grow when palette is changed. 
# same goes for anything that calls the method of self.font().pointSize()
# to get the current size, as its increased every time we set the font
self.setFont(0/1,font)

If we set the values in the __init__


self.fontPointSize = partial(self.font(0).pointSize) # set only once when class initialized 
self.fontPointSize = self.font(0).pointSize          # set only once when class initialized
# we increment with the same pointSize every time updateDepth is run
font.setPointSize(self.fontPointSize()+1) 
# when the font is set it will always use size 14(or whatever the actual size is)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the issue then is that self.font(0) returns a copy of (instead of a reference to) a QFont?
From the Qt docs:

QFont QTreeWidgetItem::font(int column) const

h, s, l, a = color.getHslF()
lightness = 0.5 + (l - 0.5) * .8
background = QtGui.QColor.fromHslF(h, s, lightness, a)
altBackground = color
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, but you might be able to help future devs to figure this out faster.
What you now call "color" is the default background color.
What you call "background" is a modified background color.

Maybe swapping the names around makes that easier to follow (and saves a line):

background = palette.base().color()
h, s, l, a = background.getHslF()
lightness = 0.5 + (l - 0.5) * .8
altBackground = QtGui.QColor.fromHslF(h, s, lightness, a)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I did it this way because I didn't want to call setBackground(altBackground), pick between using setBackground more than once, or writing an else.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd be more confused seeing setBackground(altBackground) and having to trace my way through the code to figure out why it wasn't called with background

if depth == 0:
background = altBackground
font.setPointSize(self.fontPointSize() + 1)
self.setBackground(c, background)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you swap above, these need to swap, too :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just put

 if depth == 0:
    font.setPointSize(self.fontPointSize() + 1)
    self.setBackground(c, background)
else:
    self.setBackground(c, altBackground)`

Then it should be clear what background goes where.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return c


def applyPalette(app,name):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs documentation before merge, but we can work that out when we know that this is the way we want to do things.
(I like it.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm changing the name parameter to style. Do you think we should set a default where style = qarkstyleDark?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want people to read the docs on this, so I think it is better not to set a default.
We should accept a QPalette for the style parameter, though, and we should check the base color to update the darkMode flag!

Copy link
Copy Markdown
Contributor Author

@Wubbzi Wubbzi Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should accept a QPalette for the style parameter, though, and we should check the base color to update the darkMode flag!

Do you mean in addition to accepting a string, like this?

def applyPalette(app, style):
    if isinstance(style, str):
        style = style.lower()
        if style == 'qdarkstylelight':
            p = palette.getQDarkStyleLightQPalette()
        elif style in ['qdarkstyle','qdarkstyledark']:
            p = palette.getQDarkStyleDarkQPalette()
        else:
            raise ValueError(f'no palette by the name {style} exists')
    elif isinstance(style, QtGui.QPalette):
        p = style
        app.setPalette(p)
    else:
        raise TypeError('style should either be a string or QPalette')
    app.setPalette(p)

Copy link
Copy Markdown
Contributor

@NilsNemitz NilsNemitz Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Except for not setting the palette twice, maybe.

And with the overall addition of the basecolor lightness check from mkQApp.
That would go just before or after the final setPalette call, when we know that p contains a QPalette.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Except for not setting the palette twice, maybe.

I just wanted to make sure it was really set. Like really set...
Fixed!

And with the overall addition of the basecolor lightness check from mkQApp.
That would go just before or after the final setPalette call, when we know that p contains a QPalette.

Gotcha will emit the palette through paletteChanged, though... is the darkMode property used other than the example app?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, if we catch the change there and set the flag, then we're all good!

Let's assume for now that some part of the code somewhere probably does something with the darkMode flag. So it's nice to always have it up-to-date.


def applyPalette(app,name):
name = name.lower()
if name == "light":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to advertise (in the docs) a name that is specific to QDarkStyle here.
'qdarkstyle' and 'qdarkstylelight' come to mind.
We can default 'dark' and 'light' to these, but we might point these simple names at palettes that do not depend on a stylesheet in the future?
To clarify
if name in ['qdarkstylelight', 'light']:
and
if name in ['qdarkstyle','dark']:
seem more appealing to me.

The docs will only mention the qdarkstyle names for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid confusion in the future about why the 'dark' palette suddenly looks different with the qdarkstyle dark palette stylesheet, should we just stick with names specific to qdarkstyle?

if name == "qdarkstylelight":
    ...
elif name =="qdarkstyledark"
    ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those would be good canonical names.
I would tend towards "qdarkstyle" for the dark style, that seems less redundant.
I wouldn't mind an alias to "qdarkstyledark".

You are probably right about not having a 'dark' label until we think about that some more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, the changed to:

        if style == 'qdarkstylelight':
            p = palette.getQDarkStyleLightQPalette()
        elif style in ['qdarkstyle','qdarkstyledark']:
            p = palette.getQDarkStyleDarkQPalette()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that :)


def updatePalette(self):
"""
- called when application palette changes -
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure that this is what makes sphinx confused, and that
"Called when the application palette changes"
would not cause any trouble.

@Wubbzi Wubbzi marked this pull request as ready for review December 3, 2021 02:59
@j9ac9k j9ac9k merged commit 074d472 into pyqtgraph:master Dec 11, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants