helloSystem / Filer

A file manager that can also render the desktop
GNU General Public License v2.0
30 stars 9 forks source link

Draw shadow gradient onto wallpaper on main screen #142

Open probonopd opened 3 years ago

probonopd commented 3 years ago

image

The question is: How can we get the window for m_fakeWidget (or another window) displayed behind the real menu window, but in a way that its drop shadow gets rendered by KWin.

louies0623 commented 2 years ago

I think you only need to split the Menu Bar and Options menu. This should solve the problem. Idea menu bar

probonopd commented 2 years ago

Thinking a bit more about it, Filer could probably render the shadow as part of when it renders the wallpaper.

louies0623 commented 2 years ago

Can you let Filter render the wallpaper to temporarily hide the menu bar while rendering the shadow, and then move the Menu bar from top to bottom with special effects?

louies0623 commented 2 years ago

Just like this, it can temporarily hide the problem when the Menu bar renders the shadow. https://user-images.githubusercontent.com/44593430/136714815-940a0114-5a6e-49fb-a6b2-d5148966a7c2.mp4

probonopd commented 2 years ago

Right, the animation. If we want the animation then having Filer involved in rendering the shadow is a bad idea. We need to find a way for Menu to render the shadow itself. Most likely by using a "dummy window" (m_fakeWidget) which is "behind" the real Menu window.

The m_fakeWidget "dummy window" - how can we get it "behind" the real menu bar?

image

/*
 * Copyright (C) 2020 PandaOS Team.
 *
 * Author:     rekols <revenmartin@gmail.com>
 * Portions:   probono <probono@puredarwin.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "mainwindow.h"
#include <QApplication>
#include <QHBoxLayout>
#include <QScreen>
#include <QPainter>
#include <QPainterPath>
#include <QDebug>
#include <QApplication>
#include <QLibraryInfo>
#include <KF5/KWindowSystem/KWindowSystem>

#include <QX11Info>
#include <QScreen>
#include <xcb/xcb.h>
#include <X11/Xlib.h>

MainWindow::MainWindow(QWidget *parent)
    : QFrame(parent),
      m_fakeWidget(new QWidget(nullptr)),
      m_mainPanel(new MainPanel)
{
    this->setObjectName("menuBar");
    // Install the translations built-into Qt itself
    qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath));
    qApp->installTranslator(&qtTranslator);

    // Install our own translations
    translator1.load("menubar_" + QLocale::system().name(), QCoreApplication::applicationDirPath() + QString("/../share/menubar/translations/")); // probono: FHS-like path relative to main binary
    qApp->installTranslator(&translator1);
    translator2.load("menubar_" + QLocale::system().name(), QCoreApplication::applicationDirPath()); // probono: When qm files are next to the executable ("uninstalled"), useful during development
    qApp->installTranslator(&translator2);

    QHBoxLayout *layout = new QHBoxLayout;
    layout->addSpacing(10);
    layout->addWidget(m_mainPanel);
    layout->addSpacing(10);
    layout->setMargin(0);
    layout->setSpacing(0);
    setLayout(layout);

    // m_fakeWidget->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowDoesNotAcceptFocus | Qt::SplashScreen);
    m_fakeWidget->setWindowFlags(Qt::WindowDoesNotAcceptFocus);
    // m_fakeWidget->setAttribute(Qt::WA_TranslucentBackground);

    // Prevent menubar from becoming faded/translucent if we use a compositing manager
    // that fades/makes translucent inactive windows
    m_mainPanel->setWindowFlags(Qt::WindowDoesNotAcceptFocus);

    setAttribute(Qt::WA_NoSystemBackground, false);
    // setAttribute(Qt::WA_TranslucentBackground);
    setWindowFlags(Qt::FramelessWindowHint);

    KWindowSystem::setOnDesktop(effectiveWinId(), NET::OnAllDesktops);

    // "Indicates a toplevel menu (AKA macmenu).
    // This is a KDE extension to the _NET_WM_WINDOW_TYPE mechanism."
    // Source:
    // https://api.kde.org/frameworks/kwindowsystem/html/classNET.html#a4b3115c0f40e7bc8e38119cc44dd60e0
    // Can be inspected with: xwininfo -wm, it contains "Window type: Kde Net Wm Window Type Topmenu"
    // This should allow e.g., picom to set different settings regarding shadows and transparency
    KWindowSystem::setType(winId(), NET::TopMenu);

    //TODO:
    //Call this when the user sets the primary display via xrandr
    initSize();

    //subscribe to changes on our display like if we change the screen resolution, orientation etc..
    connect(qApp->primaryScreen(), &QScreen::geometryChanged, this, &MainWindow::initSize);
    connect(qApp->primaryScreen(), &QScreen::orientationChanged, this, &MainWindow::initSize);
    connect(qApp->primaryScreen(), &QScreen::virtualGeometryChanged, this, &MainWindow::initSize);
    connect(qApp->primaryScreen(), &QScreen::availableGeometryChanged, this, &MainWindow::initSize);
    connect(qApp->primaryScreen(), &QScreen::logicalDotsPerInchChanged, this, &MainWindow::initSize);
    connect(qApp->primaryScreen(), &QScreen::physicalDotsPerInchChanged, this, &MainWindow::initSize);
    connect(qApp->primaryScreen(), &QScreen::physicalSizeChanged, this, &MainWindow::initSize);
    connect(qApp->primaryScreen(), &QScreen::primaryOrientationChanged, this, &MainWindow::initSize);

    // Appear with an animation
    QPropertyAnimation *animation = new QPropertyAnimation(this, "pos");
    animation->setDuration(1500);
    animation->setStartValue(QPoint(qApp->primaryScreen()->geometry().x(), -2 * qApp->primaryScreen()->geometry().height()));
    animation->setEndValue(QPoint(qApp->primaryScreen()->geometry().x(),qApp->primaryScreen()->geometry().y()));
    animation->setEasingCurve(QEasingCurve::OutCubic);
    animation->start(QPropertyAnimation::DeleteWhenStopped);
    this->activateWindow(); // probono: Ensure that we have the focus when menu is launched so that one can enter text in the search box
    m_mainPanel->raise(); // probono: Trying to give typing focus to the search box that is in there. Needed? Does not seem tp hurt

}

MainWindow::~MainWindow()
{

}

void MainWindow::paintEvent(QPaintEvent *e)
{

    // probono: Draw black rounded corners on the top edges
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing);
    p.setPen(Qt::NoPen);
    int round_pixels = 5; // like /usr/local/etc/xdg/picom.conf // probono: Make this relative to the height of the MainWindow?
    // QPainterPath::subtracted() takes InnerPath and subtracts it from OuterPath to produce the final shape
    QPainterPath OuterPath;
    OuterPath.addRect(0, 0, qApp->primaryScreen()->geometry().width(), 2*round_pixels);
    QPainterPath InnerPath;
    InnerPath.addRoundedRect(QRect(0, 0, qApp->primaryScreen()->geometry().width(), 4*round_pixels), round_pixels, round_pixels);
    QPainterPath FillPath;
    FillPath = OuterPath.subtracted(InnerPath);
    p.fillPath(FillPath, Qt::black);

    // Draw the other widgets
    QWidget::paintEvent(e);
}

void MainWindow::initSize()
{
    QRect primaryRect = qApp->primaryScreen()->geometry();

    setFixedWidth(primaryRect.width());

    // probono: Construct a populated(!) QMenuBar so that we can determine
    // its height and use the same height for the MainWindow. Is there a better way?
    QMenuBar *dummyMenuBar = new QMenuBar;
    dummyMenuBar->setContentsMargins(0, 0, 0, 0);
    dummyMenuBar->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
    QMenu *dummyMenu = new QMenu;
    dummyMenuBar->addMenu(dummyMenu);
    qDebug() << "probono: dummyMenu->sizeHint().height():" << dummyMenu->sizeHint().height();
    setFixedHeight(dummyMenuBar->sizeHint().height());

    //move this to the active screen and xrandr position
    move(qApp->primaryScreen()->geometry().x(), qApp->primaryScreen()->geometry().y());

    setStrutPartial();

    KWindowSystem::setState(winId(), NET::SkipTaskbar); // Do not show in Dock
    KWindowSystem::setState(winId(), NET::StaysOnTop);
    KWindowSystem::setState(winId(), NET::SkipPager);
    KWindowSystem::setState(winId(), NET::SkipSwitcher);
    // How can we set _NET_WM_STATE_ABOVE? KDE krunner has it set

    // https://stackoverflow.com/a/27964691
    // "window should be of type _NET_WM_TYPE_DOCK and you must first map it then move it
    // to position, otherwise the WM may sometimes place it outside of it own strut."
    // _NET_WM_WINDOW_TYPE_DOCK
    KWindowSystem::setType(winId(), NET::Dock);

    // probono: Set background gradient
    // Commenting this out because possibly this interferes with theming via a QSS file via QtPlugin?
    // this->setStyleSheet( "MainWindow { background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #fff, stop: 0.1 #eee, stop: 0.39 #eee, stop: 0.4 #ddd, stop: 1 #eee); }");
}

void MainWindow::setStrutPartial()
{
    //不清真的作法,kwin设置blur后设置程序支撑导致模糊无效
    //TRANSLATED Unclear practice, setting program support after kwin set blur causes blur invalid
    QRect r(geometry());
    m_fakeWidget->setFixedHeight(this->height());
    m_fakeWidget->setFixedWidth(this->width());
    m_fakeWidget->move(10,10);
    // m_fakeWidget->setGeometry(r);
    m_fakeWidget->setVisible(true);

    const QRect windowRect = this->rect();
    NETExtendedStrut strut;

    strut.top_width = height(); // + 1; // 1 pixel between menu bar and maximized window not needed if we have a shadow
    strut.top_start = x();
    strut.top_end = x() + width();

    KWindowSystem::setExtendedStrut(m_fakeWidget->winId(),
                                     strut.left_width,
                                     strut.left_start,
                                     strut.left_end,
                                     strut.right_width,
                                     strut.right_start,
                                     strut.right_end,
                                     strut.top_width,
                                     strut.top_start,
                                     strut.top_end,
                                     strut.bottom_width,
                                     strut.bottom_start,
                                     strut.bottom_end);
}
louies0623 commented 2 years ago

It should be "forced to be ranked first" like a right-click menu or a sticky note. On the Menubar Menu.

kettle-7 commented 2 years ago

So would always on top be better or does kwin do that with _NET_WM_DOCK?

probonopd commented 2 years ago

The real menu is always on top. But we don't want the shadow to be rendered by that window, otherwise it would be rendered over windows that are close to the menu. Hence, we want another (dummy) window behind the real window, and that should always be at the bottom and have the shadow.

image

louies0623 commented 2 years ago

If the shadow is a transparent gradient image under the window layer instead of being generated by drawing, I think this will save the memory usage of global menu shadow generation.

But pay attention to this tip about drawing. efgs

probonopd commented 2 years ago

The solution is quite simple if you think about it.

Let the code that draws the desktop also draw the shadow for the menu. Since per the Human Interface Guidelines the menu must never go away (except for an application being in fullscreen, in which case you won't see the desktop), this is a perfectly acceptable solution.

Code along the lines of (not quite working yet but you should get the idea):

#include <QGraphicsEffect>

  QRect shadowRect(QPoint(0, 22), QSize(1600, 10)); // TODO: Screen width instead of 1600
  QLinearGradient alphaGradient(shadowRect.topLeft(), shadowRect.bottomLeft());
  alphaGradient.setColorAt(0.0, Qt::black);
  alphaGradient.setColorAt(1.0, Qt::transparent);
  QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect;
  effect->setOpacityMask(alphaGradient);
  painter->fillRect(menuRect,  effect);

But where to put it in?

desktopitemdelegate.cpp has a ::paint method where we can put it in, desktopwindow.cpp hasn't.

The following is obviously not the correct way to do it but it is a working except for color gradients instead of transparency gradients being used.

menu_shadow

void DesktopItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
  Q_ASSERT(index.isValid());
  QStyleOptionViewItem opt = option;
  initStyleOption(&opt, index);

  QPen origPen = painter->pen();
  QRect menuRect(QPoint(0, 22), QSize(1600, 10));
  QLinearGradient m_gradient(0,22,0,32);
  m_gradient.setColorAt(0.0, Qt::darkGray);
  m_gradient.setColorAt(1.0, Qt::white);
  painter->fillRect(menuRect,  m_gradient);
  painter->setPen(origPen);
  painter->save();
  painter->setClipRect(option.rect);

Who knows enough about Qt to help get it done?

probonopd commented 2 years ago

Now using the proper transparency gradient. Actually easy, just define a QColor with R, G, B, Alpha.

Adding the following to desktopwindow.cpp:

void DesktopWindow::paintEvent(QPaintEvent *)
{
    // This gets drawn but BEHIND the wallpaper. FIXME: Draw above wallpaper
    qDebug() << Q_FUNC_INFO;
    QPainter painter(this);
    QPen origPen = painter.pen();
    QRect menuRect(QPoint(0, 22), QSize(1600, 30));
    QLinearGradient m_gradient(0,22,0,52);
    m_gradient.setColorAt(0.0, QColor(128, 128, 128, 255));
    m_gradient.setColorAt(1.0, QColor(128, 128, 128, 0));
    painter.fillRect(menuRect,  m_gradient);
    painter.setPen(origPen);
    painter.save();
}

The object gets drawn, but behind the wallpaper. Hence it is only visible if the wallpaper is set to transparent.

image

We need to do this differently.

Maybe draw at the end of DesktopWindow::updateWallpaper()? Doesnt seem to work...

Same if I put it into void FolderView::paintEvent(QPaintEvent *) of folderview.cpp.

probonopd commented 1 year ago

Maybe we need to create a pixmap that contains the wallpaper and the shadow, and use the combined pixmap instead of just the wallpaper image.

probonopd commented 1 year ago

As a temporary workaround, using a wallpaper image that has the shadow "built in":

https://github.com/helloSystem/ISO/releases/download/assets/graphite_shadow.jpg

Kinda crude but effective.

Let's not forget to undo that workaround once we found a proper solution for drawing the shadow using Qt.

probonopd commented 1 year ago

This is it:

  QPen origPen = painter->pen();
  QRect shadowRect(QPoint(0, 0), QSize(1600, 33));
  QLinearGradient linearGradient(0,0,0,33);
  linearGradient.setColorAt(0.00, QColor::fromRgbF(0, 0, 0, 0.3));
  linearGradient.setColorAt(0.33, QColor::fromRgbF(0, 0, 0, 0.2));
  linearGradient.setColorAt(1.00, QColor::fromRgbF(0, 0, 0, 0.0));
  painter->fillRect(shadowRect,  linearGradient);
  painter->setPen(origPen);
  painter->save();
  painter->setClipRect(option.rect);

The only remaining question: Where to put this.

louies0623 commented 1 year ago

@probonopd I recently found how to solve the problem of shadow coverage, Ubuntu Unity also has this problem, but it is well adjusted, that is, to reduce the spread of Kwin shadows. This also reduces system power consumption in window shadow generation.

VirtualBox_PC-1_25_12_2022_19_42_01