diff --git a/colorcorrection/manager.cpp b/colorcorrection/manager.cpp index 182265191..2a90945d5 100644 --- a/colorcorrection/manager.cpp +++ b/colorcorrection/manager.cpp @@ -1,781 +1,832 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright 2017 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "manager.h" #include "colorcorrectdbusinterface.h" #include "suncalc.h" #include #include #include #include #include #include #include #include -#include +#include +#include + +#include #include #include +#include #ifdef Q_OS_LINUX #include #endif #include #include namespace KWin { namespace ColorCorrect { static const int QUICK_ADJUST_DURATION = 2000; static const int TEMPERATURE_STEP = 50; static bool checkLocation(double lat, double lng) { return -90 <= lat && lat <= 90 && -180 <= lng && lng <= 180; } Manager::Manager(QObject *parent) : QObject(parent) { m_iface = new ColorCorrectDBusInterface(this); connect(kwinApp(), &Application::workspaceCreated, this, &Manager::init); } void Manager::init() { Settings::instance(kwinApp()->config()); // we may always read in the current config readConfig(); if (!kwinApp()->platform()->supportsGammaControl()) { // at least update the sun timings to make the values accessible via dbus updateSunTimings(true); return; } connect(Screens::self(), &Screens::countChanged, this, &Manager::hardReset); connect(LogindIntegration::self(), &LogindIntegration::sessionActiveChanged, this, [this](bool active) { if (active) { hardReset(); } else { cancelAllTimers(); } } ); #ifdef Q_OS_LINUX // monitor for system clock changes - from the time dataengine auto timeChangedFd = ::timerfd_create(CLOCK_REALTIME, O_CLOEXEC | O_NONBLOCK); ::itimerspec timespec; //set all timers to 0, which creates a timer that won't do anything ::memset(×pec, 0, sizeof(timespec)); // Monitor for the time changing (flags == TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET). // However these are not exposed in glibc so value is hardcoded: ::timerfd_settime(timeChangedFd, 3, ×pec, 0); connect(this, &QObject::destroyed, [timeChangedFd]() { ::close(timeChangedFd); }); auto notifier = new QSocketNotifier(timeChangedFd, QSocketNotifier::Read, this); connect(notifier, &QSocketNotifier::activated, this, [this](int fd) { uint64_t c; ::read(fd, &c, 8); // check if we're resuming from suspend - in this case do a hard reset // Note: We're using the time clock to detect a suspend phase instead of connecting to the // provided logind dbus signal, because this signal would be received way too late. QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.DBus.Properties", QStringLiteral("Get")); message.setArguments(QVariantList({"org.freedesktop.login1.Manager", QStringLiteral("PreparingForSleep")})); QDBusReply reply = QDBusConnection::systemBus().call(message); bool comingFromSuspend; if (reply.isValid()) { comingFromSuspend = reply.value().toBool(); } else { qCDebug(KWIN_COLORCORRECTION) << "Failed to get PreparingForSleep Property of logind session:" << reply.error().message(); // Always do a hard reset in case we have no further information. comingFromSuspend = true; } if (comingFromSuspend) { hardReset(); } else { resetAllTimers(); } }); #else // TODO: Alternative method for BSD. #endif hardReset(); } void Manager::hardReset() { cancelAllTimers(); updateSunTimings(true); if (kwinApp()->platform()->supportsGammaControl() && m_active) { m_running = true; commitGammaRamps(currentTargetTemp()); } resetAllTimers(); } void Manager::reparseConfigAndReset() { cancelAllTimers(); readConfig(); hardReset(); } +// FIXME: The internal OSD service doesn't work on X11 right now. Once the QPA +// is ported away from Wayland, drop this function in favor of the internal +// OSD service. +static void showStatusOsd(bool enabled) +{ + // TODO: Maybe use different icons? + const QString iconName = enabled + ? QStringLiteral("preferences-desktop-display-nightcolor-on") + : QStringLiteral("preferences-desktop-display-nightcolor-off"); + + const QString text = enabled + ? i18nc("Night Color was enabled", "Night Color On") + : i18nc("Night Color was disabled", "Night Color Off"); + + QDBusMessage message = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("showText")); + message.setArguments({ iconName, text }); + + QDBusConnection::sessionBus().asyncCall(message); +} + +void Manager::toggle() +{ + if (!kwinApp()->platform()->supportsGammaControl()) { + return; + } + + m_active = !m_active; + + showStatusOsd(m_active); + + resetAllTimers(); +} + +void Manager::initShortcuts() +{ + QAction *toggleAction = new QAction(this); + toggleAction->setProperty("componentName", QStringLiteral(KWIN_NAME)); + toggleAction->setObjectName(i18n("Toggle Night Color")); + toggleAction->setText(i18n("Toggle Night Color")); + KGlobalAccel::setGlobalShortcut(toggleAction, QList()); + input()->registerShortcut(QKeySequence(), toggleAction, this, &Manager::toggle); +} + void Manager::readConfig() { Settings *s = Settings::self(); s->load(); m_active = s->active(); NightColorMode mode = s->mode(); if (mode == NightColorMode::Location || mode == NightColorMode::Timings) { m_mode = mode; } else { // also fallback for invalid setting values m_mode = NightColorMode::Automatic; } m_nightTargetTemp = qBound(MIN_TEMPERATURE, s->nightTemperature(), NEUTRAL_TEMPERATURE); double lat, lng; auto correctReadin = [&lat, &lng]() { if (!checkLocation(lat, lng)) { // out of domain lat = 0; lng = 0; } }; // automatic lat = s->latitudeAuto(); lng = s->longitudeAuto(); correctReadin(); m_latAuto = lat; m_lngAuto = lng; // fixed location lat = s->latitudeFixed(); lng = s->longitudeFixed(); correctReadin(); m_latFixed = lat; m_lngFixed = lng; // fixed timings QTime mrB = QTime::fromString(s->morningBeginFixed(), "hhmm"); QTime evB = QTime::fromString(s->eveningBeginFixed(), "hhmm"); int diffME = mrB.msecsTo(evB); if (diffME <= 0) { // morning not strictly before evening - use defaults mrB = QTime(6,0); evB = QTime(18,0); diffME = mrB.msecsTo(evB); } int diffMin = qMin(diffME, MSC_DAY - diffME); int trTime = s->transitionTime() * 1000 * 60; if (trTime < 0 || diffMin <= trTime) { // transition time too long - use defaults mrB = QTime(6,0); evB = QTime(18,0); trTime = FALLBACK_SLOW_UPDATE_TIME; } m_morning = mrB; m_evening = evB; m_trTime = qMax(trTime / 1000 / 60, 1); } void Manager::resetAllTimers() { cancelAllTimers(); if (kwinApp()->platform()->supportsGammaControl()) { if (m_active) { m_running = true; } // we do this also for active being false in order to reset the temperature back to the day value resetQuickAdjustTimer(); } else { m_running = false; } } void Manager::cancelAllTimers() { delete m_slowUpdateStartTimer; delete m_slowUpdateTimer; delete m_quickAdjustTimer; m_slowUpdateStartTimer = nullptr; m_slowUpdateTimer = nullptr; m_quickAdjustTimer = nullptr; } void Manager::resetQuickAdjustTimer() { updateSunTimings(false); int tempDiff = qAbs(currentTargetTemp() - m_currentTemp); // allow tolerance of one TEMPERATURE_STEP to compensate if a slow update is coincidental if (tempDiff > TEMPERATURE_STEP) { cancelAllTimers(); m_quickAdjustTimer = new QTimer(this); m_quickAdjustTimer->setSingleShot(false); connect(m_quickAdjustTimer, &QTimer::timeout, this, &Manager::quickAdjust); int interval = QUICK_ADJUST_DURATION / (tempDiff / TEMPERATURE_STEP); if (interval == 0) { interval = 1; } m_quickAdjustTimer->start(interval); } else { resetSlowUpdateStartTimer(); } } void Manager::quickAdjust() { if (!m_quickAdjustTimer) { return; } int nextTemp; int targetTemp = currentTargetTemp(); if (m_currentTemp < targetTemp) { nextTemp = qMin(m_currentTemp + TEMPERATURE_STEP, targetTemp); } else { nextTemp = qMax(m_currentTemp - TEMPERATURE_STEP, targetTemp); } commitGammaRamps(nextTemp); if (nextTemp == targetTemp) { // stop timer, we reached the target temp delete m_quickAdjustTimer; m_quickAdjustTimer = nullptr; resetSlowUpdateStartTimer(); } } void Manager::resetSlowUpdateStartTimer() { delete m_slowUpdateStartTimer; m_slowUpdateStartTimer = nullptr; if (!m_running || m_quickAdjustTimer) { // only reenable the slow update start timer when quick adjust is not active anymore return; } // set up the next slow update m_slowUpdateStartTimer = new QTimer(this); m_slowUpdateStartTimer->setSingleShot(true); connect(m_slowUpdateStartTimer, &QTimer::timeout, this, &Manager::resetSlowUpdateStartTimer); updateSunTimings(false); int diff; if (m_mode == NightColorMode::Timings) { // Timings mode is in local time diff = QDateTime::currentDateTime().msecsTo(m_next.first); } else { diff = QDateTime::currentDateTimeUtc().msecsTo(m_next.first); } if (diff <= 0) { qCCritical(KWIN_COLORCORRECTION) << "Error in time calculation. Deactivating Night Color."; return; } m_slowUpdateStartTimer->start(diff); // start the current slow update resetSlowUpdateTimer(); } void Manager::resetSlowUpdateTimer() { delete m_slowUpdateTimer; m_slowUpdateTimer = nullptr; QDateTime now = QDateTime::currentDateTimeUtc(); bool isDay = daylight(); int targetTemp = isDay ? m_dayTargetTemp : m_nightTargetTemp; if (m_prev.first == m_prev.second) { // transition time is zero commitGammaRamps(isDay ? m_dayTargetTemp : m_nightTargetTemp); return; } if (m_prev.first <= now && now <= m_prev.second) { int availTime = now.msecsTo(m_prev.second); m_slowUpdateTimer = new QTimer(this); m_slowUpdateTimer->setSingleShot(false); if (isDay) { connect(m_slowUpdateTimer, &QTimer::timeout, this, [this]() {slowUpdate(m_dayTargetTemp);}); } else { connect(m_slowUpdateTimer, &QTimer::timeout, this, [this]() {slowUpdate(m_nightTargetTemp);}); } // calculate interval such as temperature is changed by TEMPERATURE_STEP K per timer timeout int interval = availTime / (qAbs(targetTemp - m_currentTemp) / TEMPERATURE_STEP); if (interval == 0) { interval = 1; } m_slowUpdateTimer->start(interval); } } void Manager::slowUpdate(int targetTemp) { if (!m_slowUpdateTimer) { return; } int nextTemp; if (m_currentTemp < targetTemp) { nextTemp = qMin(m_currentTemp + TEMPERATURE_STEP, targetTemp); } else { nextTemp = qMax(m_currentTemp - TEMPERATURE_STEP, targetTemp); } commitGammaRamps(nextTemp); if (nextTemp == targetTemp) { // stop timer, we reached the target temp delete m_slowUpdateTimer; m_slowUpdateTimer = nullptr; } } void Manager::updateSunTimings(bool force) { QDateTime todayNow = QDateTime::currentDateTimeUtc(); if (m_mode == NightColorMode::Timings) { QDateTime todayNowLocal = QDateTime::currentDateTime(); QDateTime morB = QDateTime(todayNowLocal.date(), m_morning); QDateTime morE = morB.addSecs(m_trTime * 60); QDateTime eveB = QDateTime(todayNowLocal.date(), m_evening); QDateTime eveE = eveB.addSecs(m_trTime * 60); if (morB <= todayNowLocal && todayNowLocal < eveB) { m_next = DateTimes(eveB, eveE); m_prev = DateTimes(morB, morE); } else if (todayNowLocal < morB) { m_next = DateTimes(morB, morE); m_prev = DateTimes(eveB.addDays(-1), eveE.addDays(-1)); } else { m_next = DateTimes(morB.addDays(1), morE.addDays(1)); m_prev = DateTimes(eveB, eveE); } return; } double lat, lng; if (m_mode == NightColorMode::Automatic) { lat = m_latAuto; lng = m_lngAuto; } else { lat = m_latFixed; lng = m_lngFixed; } if (!force) { // first try by only switching the timings if (daylight()) { // next is morning m_prev = m_next; m_next = getSunTimings(todayNow.date().addDays(1), lat, lng, true); } else { // next is evening m_prev = m_next; m_next = getSunTimings(todayNow.date(), lat, lng, false); } } if (force || !checkAutomaticSunTimings()) { // in case this fails, reset them DateTimes morning = getSunTimings(todayNow.date(), lat, lng, true); if (todayNow < morning.first) { m_prev = getSunTimings(todayNow.date().addDays(-1), lat, lng, false); m_next = morning; } else { DateTimes evening = getSunTimings(todayNow.date(), lat, lng, false); if (todayNow < evening.first) { m_prev = morning; m_next = evening; } else { m_prev = evening; m_next = getSunTimings(todayNow.date().addDays(1), lat, lng, true); } } } } DateTimes Manager::getSunTimings(QDate date, double latitude, double longitude, bool morning) const { Times times = calculateSunTimings(date, latitude, longitude, morning); // At locations near the poles it is possible, that we can't // calculate some or all sun timings (midnight sun). // In this case try to fallback to sensible default values. bool beginDefined = !times.first.isNull(); bool endDefined = !times.second.isNull(); if (!beginDefined || !endDefined) { if (beginDefined) { times.second = times.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); } else if (endDefined) { times.first = times.second.addMSecs( - FALLBACK_SLOW_UPDATE_TIME); } else { // Just use default values for morning and evening, but the user // will probably deactivate Night Color anyway if he is living // in a region without clear sun rise and set. times.first = morning ? QTime(6,0,0) : QTime(18,0,0); times.second = times.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); } } return DateTimes(QDateTime(date, times.first, Qt::UTC), QDateTime(date, times.second, Qt::UTC)); } bool Manager::checkAutomaticSunTimings() const { if (m_prev.first.isValid() && m_prev.second.isValid() && m_next.first.isValid() && m_next.second.isValid()) { QDateTime todayNow = QDateTime::currentDateTimeUtc(); return m_prev.first <= todayNow && todayNow < m_next.first && m_prev.first.msecsTo(m_next.first) < MSC_DAY * 23./24; } return false; } bool Manager::daylight() const { return m_prev.first.date() == m_next.first.date(); } int Manager::currentTargetTemp() const { if (!m_active) { return NEUTRAL_TEMPERATURE; } QDateTime todayNow = QDateTime::currentDateTimeUtc(); auto f = [this, todayNow](int target1, int target2) { if (todayNow <= m_prev.second) { double residueQuota = todayNow.msecsTo(m_prev.second) / (double)m_prev.first.msecsTo(m_prev.second); double ret = (int)((1. - residueQuota) * (double)target2 + residueQuota * (double)target1); // remove single digits ret = ((int)(0.1 * ret)) * 10; return (int)ret; } else { return target2; } }; if (daylight()) { return f(m_nightTargetTemp, m_dayTargetTemp); } else { return f(m_dayTargetTemp, m_nightTargetTemp); } } void Manager::commitGammaRamps(int temperature) { const auto outs = kwinApp()->platform()->outputs(); for (auto *o : outs) { int rampsize = o->gammaRampSize(); GammaRamp ramp(rampsize); /* * The gamma calculation below is based on the Redshift app: * https://github.com/jonls/redshift */ uint16_t *red = ramp.red(); uint16_t *green = ramp.green(); uint16_t *blue = ramp.blue(); // linear default state for (int i = 0; i < rampsize; i++) { uint16_t value = (double)i / rampsize * (UINT16_MAX + 1); red[i] = value; green[i] = value; blue[i] = value; } // approximate white point float whitePoint[3]; float alpha = (temperature % 100) / 100.; int bbCIndex = ((temperature - 1000) / 100) * 3; whitePoint[0] = (1. - alpha) * blackbodyColor[bbCIndex] + alpha * blackbodyColor[bbCIndex + 3]; whitePoint[1] = (1. - alpha) * blackbodyColor[bbCIndex + 1] + alpha * blackbodyColor[bbCIndex + 4]; whitePoint[2] = (1. - alpha) * blackbodyColor[bbCIndex + 2] + alpha * blackbodyColor[bbCIndex + 5]; for (int i = 0; i < rampsize; i++) { red[i] = qreal(red[i]) / (UINT16_MAX+1) * whitePoint[0] * (UINT16_MAX+1); green[i] = qreal(green[i]) / (UINT16_MAX+1) * whitePoint[1] * (UINT16_MAX+1); blue[i] = qreal(blue[i]) / (UINT16_MAX+1) * whitePoint[2] * (UINT16_MAX+1); } if (o->setGammaRamp(ramp)) { m_currentTemp = temperature; m_failedCommitAttempts = 0; } else { m_failedCommitAttempts++; if (m_failedCommitAttempts < 10) { qCWarning(KWIN_COLORCORRECTION).nospace() << "Committing Gamma Ramp failed for output " << o->name() << ". Trying " << (10 - m_failedCommitAttempts) << " times more."; } else { // TODO: On multi monitor setups we could try to rollback earlier changes for already commited outputs qCWarning(KWIN_COLORCORRECTION) << "Gamma Ramp commit failed too often. Deactivating color correction for now."; m_failedCommitAttempts = 0; // reset so we can try again later (i.e. after suspend phase or config change) m_running = false; cancelAllTimers(); } } } } QHash Manager::info() const { return QHash { { QStringLiteral("Available"), kwinApp()->platform()->supportsGammaControl() }, { QStringLiteral("ActiveEnabled"), true}, { QStringLiteral("Active"), m_active}, { QStringLiteral("ModeEnabled"), true}, { QStringLiteral("Mode"), (int)m_mode}, { QStringLiteral("NightTemperatureEnabled"), true}, { QStringLiteral("NightTemperature"), m_nightTargetTemp}, { QStringLiteral("Running"), m_running}, { QStringLiteral("CurrentColorTemperature"), m_currentTemp}, { QStringLiteral("LatitudeAuto"), m_latAuto}, { QStringLiteral("LongitudeAuto"), m_lngAuto}, { QStringLiteral("LocationEnabled"), true}, { QStringLiteral("LatitudeFixed"), m_latFixed}, { QStringLiteral("LongitudeFixed"), m_lngFixed}, { QStringLiteral("TimingsEnabled"), true}, { QStringLiteral("MorningBeginFixed"), m_morning.toString(Qt::ISODate)}, { QStringLiteral("EveningBeginFixed"), m_evening.toString(Qt::ISODate)}, { QStringLiteral("TransitionTime"), m_trTime}, }; } bool Manager::changeConfiguration(QHash data) { bool activeUpdate, modeUpdate, tempUpdate, locUpdate, timeUpdate; activeUpdate = modeUpdate = tempUpdate = locUpdate = timeUpdate = false; bool active = m_active; NightColorMode mode = m_mode; int nightT = m_nightTargetTemp; double lat = m_latFixed; double lng = m_lngFixed; QTime mor = m_morning; QTime eve = m_evening; int trT = m_trTime; QHash::const_iterator iter1, iter2, iter3; iter1 = data.constFind("Active"); if (iter1 != data.constEnd()) { if (!iter1.value().canConvert()) { return false; } bool act = iter1.value().toBool(); activeUpdate = m_active != act; active = act; } iter1 = data.constFind("Mode"); if (iter1 != data.constEnd()) { if (!iter1.value().canConvert()) { return false; } int mo = iter1.value().toInt(); if (mo < 0 || 2 < mo) { return false; } NightColorMode moM; switch (mo) { case 0: moM = NightColorMode::Automatic; break; case 1: moM = NightColorMode::Location; break; case 2: moM = NightColorMode::Timings; } modeUpdate = m_mode != moM; mode = moM; } iter1 = data.constFind("NightTemperature"); if (iter1 != data.constEnd()) { if (!iter1.value().canConvert()) { return false; } int nT = iter1.value().toInt(); if (nT < MIN_TEMPERATURE || NEUTRAL_TEMPERATURE < nT) { return false; } tempUpdate = m_nightTargetTemp != nT; nightT = nT; } iter1 = data.constFind("LatitudeFixed"); iter2 = data.constFind("LongitudeFixed"); if (iter1 != data.constEnd() && iter2 != data.constEnd()) { if (!iter1.value().canConvert() || !iter2.value().canConvert()) { return false; } double la = iter1.value().toDouble(); double ln = iter2.value().toDouble(); if (!checkLocation(la, ln)) { return false; } locUpdate = m_latFixed != la || m_lngFixed != ln; lat = la; lng = ln; } iter1 = data.constFind("MorningBeginFixed"); iter2 = data.constFind("EveningBeginFixed"); iter3 = data.constFind("TransitionTime"); if (iter1 != data.constEnd() && iter2 != data.constEnd() && iter3 != data.constEnd()) { if (!iter1.value().canConvert() || !iter2.value().canConvert() || !iter3.value().canConvert()) { return false; } QTime mo = QTime::fromString(iter1.value().toString(), Qt::ISODate); QTime ev = QTime::fromString(iter2.value().toString(), Qt::ISODate); if (!mo.isValid() || !ev.isValid()) { return false; } int tT = iter3.value().toInt(); int diffME = mo.msecsTo(ev); if (diffME <= 0 || qMin(diffME, MSC_DAY - diffME) <= tT * 60 * 1000 || tT < 1) { // morning not strictly before evening, transition time too long or transition time out of bounds return false; } timeUpdate = m_morning != mo || m_evening != ev || m_trTime != tT; mor = mo; eve = ev; trT = tT; } if (!(activeUpdate || modeUpdate || tempUpdate || locUpdate || timeUpdate)) { return true; } bool resetNeeded = activeUpdate || modeUpdate || tempUpdate || (locUpdate && mode == NightColorMode::Location) || (timeUpdate && mode == NightColorMode::Timings); if (resetNeeded) { cancelAllTimers(); } Settings *s = Settings::self(); if (activeUpdate) { m_active = active; s->setActive(active); } if (modeUpdate) { m_mode = mode; s->setMode(mode); } if (tempUpdate) { m_nightTargetTemp = nightT; s->setNightTemperature(nightT); } if (locUpdate) { m_latFixed = lat; m_lngFixed = lng; s->setLatitudeFixed(lat); s->setLongitudeFixed(lng); } if (timeUpdate) { m_morning = mor; m_evening = eve; m_trTime = trT; s->setMorningBeginFixed(mor.toString("hhmm")); s->setEveningBeginFixed(eve.toString("hhmm")); s->setTransitionTime(trT); } s->save(); if (resetNeeded) { resetAllTimers(); } emit configChange(info()); return true; } void Manager::autoLocationUpdate(double latitude, double longitude) { if (!checkLocation(latitude, longitude)) { return; } // we tolerate small deviations with minimal impact on sun timings if (qAbs(m_latAuto - latitude) < 2 && qAbs(m_lngAuto - longitude) < 1) { return; } cancelAllTimers(); m_latAuto = latitude; m_lngAuto = longitude; Settings *s = Settings::self(); s->setLatitudeAuto(latitude); s->setLongitudeAuto(longitude); s->save(); resetAllTimers(); emit configChange(info()); } } } diff --git a/colorcorrection/manager.h b/colorcorrection/manager.h index ae103aee7..9b4f4fa5a 100644 --- a/colorcorrection/manager.h +++ b/colorcorrection/manager.h @@ -1,147 +1,169 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright 2017 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_COLORCORRECT_MANAGER_H #define KWIN_COLORCORRECT_MANAGER_H #include "constants.h" #include #include #include #include class QTimer; namespace KWin { -class Platform; +class Workspace; namespace ColorCorrect { typedef QPair DateTimes; typedef QPair Times; class ColorCorrectDBusInterface; enum NightColorMode { // timings are based on provided location data Automatic = 0, // timings are based on fixed location data Location, // fixed timings Timings }; class KWIN_EXPORT Manager : public QObject { Q_OBJECT public: Manager(QObject *parent); void init(); /** * Get current configuration * @see changeConfiguration * @since 5.12 **/ QHash info() const; /** * Change configuration * @see info * @since 5.12 **/ bool changeConfiguration(QHash data); void autoLocationUpdate(double latitude, double longitude); + /** + * Toggles the active state of the filter. + * + * A quick transition will be started if the difference between current screen + * color temperature and target screen color temperature is too large. Target + * temperature is defined in context of the new active state. + * + * If the filter becomes inactive after calling this method, the target color + * temperature is 6500 K. + * + * If the filter becomes active after calling this method, the target screen + * color temperature is defined by the current operation mode. + * + * Note that this method is a no-op if the underlying platform doesn't support + * adjusting gamma ramps. + **/ + void toggle(); + // for auto tests void reparseConfigAndReset(); public Q_SLOTS: void resetSlowUpdateStartTimer(); void quickAdjust(); Q_SIGNALS: void configChange(QHash data); private: + void initShortcuts(); void readConfig(); void hardReset(); void slowUpdate(int targetTemp); void resetAllTimers(); int currentTargetTemp() const; void cancelAllTimers(); /** * Quick shift on manual change to current target Temperature **/ void resetQuickAdjustTimer(); /** * Slow shift to daytime target Temperature **/ void resetSlowUpdateTimer(); void updateSunTimings(bool force); DateTimes getSunTimings(QDate date, double latitude, double longitude, bool morning) const; bool checkAutomaticSunTimings() const; bool daylight() const; void commitGammaRamps(int temperature); ColorCorrectDBusInterface *m_iface; bool m_active; bool m_running = false; NightColorMode m_mode = NightColorMode::Automatic; // the previous and next sunrise/sunset intervals - in UTC time DateTimes m_prev = DateTimes(); DateTimes m_next = DateTimes(); // manual times from config QTime m_morning = QTime(6,0); QTime m_evening = QTime(18,0); int m_trTime = 30; // saved in minutes > 1 // auto location provided by work space double m_latAuto; double m_lngAuto; // manual location from config double m_latFixed; double m_lngFixed; QTimer *m_slowUpdateStartTimer = nullptr; QTimer *m_slowUpdateTimer = nullptr; QTimer *m_quickAdjustTimer = nullptr; int m_currentTemp = NEUTRAL_TEMPERATURE; int m_dayTargetTemp = NEUTRAL_TEMPERATURE; int m_nightTargetTemp = DEFAULT_NIGHT_TEMPERATURE; int m_failedCommitAttempts = 0; + + // The Workspace class needs to call initShortcuts during initialization. + friend class KWin::Workspace; }; } } #endif // KWIN_COLORCORRECT_MANAGER_H diff --git a/useractions.cpp b/useractions.cpp index 47c637985..b0befe263 100644 --- a/useractions.cpp +++ b/useractions.cpp @@ -1,1924 +1,1926 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 1999, 2000 Matthias Ettrich Copyright (C) 2003 Lubos Lunak This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ /* This file contains things relevant to direct user actions, such as responses to global keyboard shortcuts, or selecting actions from the window operations menu. */ /////////////////////////////////////////////////////////////////////////////// // NOTE: if you change the menu, keep kde-workspace/libs/taskmanager/taskactions.cpp in sync ////////////////////////////////////////////////////////////////////////////// #include "useractions.h" #include "cursor.h" #include "client.h" +#include "colorcorrection/manager.h" #include "composite.h" #include "input.h" #include "workspace.h" #include "effects.h" #include "platform.h" #include "screens.h" #include "shell_client.h" #include "virtualdesktops.h" #include "scripting/scripting.h" #ifdef KWIN_BUILD_ACTIVITIES #include "activities.h" #include #endif #include "appmenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "killwindow.h" #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif namespace KWin { UserActionsMenu::UserActionsMenu(QObject *parent) : QObject(parent) , m_menu(NULL) , m_desktopMenu(NULL) , m_screenMenu(NULL) , m_activityMenu(NULL) , m_addTabsMenu(NULL) , m_switchToTabMenu(NULL) , m_scriptsMenu(NULL) , m_resizeOperation(NULL) , m_moveOperation(NULL) , m_maximizeOperation(NULL) , m_shadeOperation(NULL) , m_keepAboveOperation(NULL) , m_keepBelowOperation(NULL) , m_fullScreenOperation(NULL) , m_noBorderOperation(NULL) , m_minimizeOperation(NULL) , m_closeOperation(NULL) , m_removeFromTabGroup(NULL) , m_closeTabGroup(NULL) , m_client(QWeakPointer()) { } UserActionsMenu::~UserActionsMenu() { discard(); } bool UserActionsMenu::isShown() const { return m_menu && m_menu->isVisible(); } bool UserActionsMenu::hasClient() const { return !m_client.isNull() && isShown(); } void UserActionsMenu::close() { if (!m_menu) { return; } m_menu->close(); m_client.clear(); } bool UserActionsMenu::isMenuClient(const AbstractClient *c) const { if (!c || m_client.isNull()) { return false; } return c == m_client.data(); } void UserActionsMenu::show(const QRect &pos, const QWeakPointer &cl) { if (!KAuthorized::authorizeAction(QStringLiteral("kwin_rmb"))) return; if (cl.isNull()) return; if (isShown()) // recursion return; if (cl.data()->isDesktop() || cl.data()->isDock()) return; m_client = cl; init(); Workspace *ws = Workspace::self(); int x = pos.left(); int y = pos.bottom(); const bool needsPopup = kwinApp()->shouldUseWaylandForCompositing(); if (y == pos.top()) { m_client.data()->blockActivityUpdates(true); if (needsPopup) { m_menu->popup(QPoint(x, y)); } else { m_menu->exec(QPoint(x, y)); } if (!m_client.isNull()) m_client.data()->blockActivityUpdates(false); } else { m_client.data()->blockActivityUpdates(true); QRect area = ws->clientArea(ScreenArea, QPoint(x, y), VirtualDesktopManager::self()->current()); menuAboutToShow(); // needed for sizeHint() to be correct :-/ int popupHeight = m_menu->sizeHint().height(); if (y + popupHeight < area.height()) { if (needsPopup) { m_menu->popup(QPoint(x, y)); } else { m_menu->exec(QPoint(x, y)); } } else { if (needsPopup) { m_menu->popup(QPoint(x, pos.top() - popupHeight)); } else { m_menu->exec(QPoint(x, pos.top() - popupHeight)); } } if (!m_client.isNull()) m_client.data()->blockActivityUpdates(false); } } void UserActionsMenu::grabInput() { m_menu->windowHandle()->setMouseGrabEnabled(true); m_menu->windowHandle()->setKeyboardGrabEnabled(true); } void UserActionsMenu::helperDialog(const QString& message, const QWeakPointer &c) { QStringList args; QString type; auto shortcut = [](const QString &name) { QAction* action = Workspace::self()->findChild(name); assert(action != NULL); const auto shortcuts = KGlobalAccel::self()->shortcut(action); return QStringLiteral("%1 (%2)").arg(action->text()) .arg(shortcuts.isEmpty() ? QString() : shortcuts.first().toString(QKeySequence::NativeText)); }; if (message == QStringLiteral("noborderaltf3")) { args << QStringLiteral("--msgbox") << i18n( "You have selected to show a window without its border.\n" "Without the border, you will not be able to enable the border " "again using the mouse: use the window operations menu instead, " "activated using the %1 keyboard shortcut.", shortcut(QStringLiteral("Window Operations Menu"))); type = QStringLiteral("altf3warning"); } else if (message == QLatin1String("fullscreenaltf3")) { args << QStringLiteral("--msgbox") << i18n( "You have selected to show a window in fullscreen mode.\n" "If the application itself does not have an option to turn the fullscreen " "mode off you will not be able to disable it " "again using the mouse: use the window operations menu instead, " "activated using the %1 keyboard shortcut.", shortcut(QStringLiteral("Window Operations Menu"))); type = QStringLiteral("altf3warning"); } else abort(); if (!type.isEmpty()) { KConfig cfg(QStringLiteral("kwin_dialogsrc")); KConfigGroup cg(&cfg, "Notification Messages"); // Depends on KMessageBox if (!cg.readEntry(type, true)) return; args << QStringLiteral("--dontagain") << QLatin1String("kwin_dialogsrc:") + type; } if (!c.isNull()) args << QStringLiteral("--embed") << QString::number(c.data()->windowId()); QtConcurrent::run([args]() { KProcess::startDetached(QStringLiteral("kdialog"), args); }); } QStringList configModules(bool controlCenter) { QStringList args; args << QStringLiteral("kwindecoration"); if (controlCenter) args << QStringLiteral("kwinoptions"); else if (KAuthorized::authorizeControlModule(QStringLiteral("kde-kwinoptions.desktop"))) args << QStringLiteral("kwinactions") << QStringLiteral("kwinfocus") << QStringLiteral("kwinmoving") << QStringLiteral("kwinadvanced") << QStringLiteral("kwinrules") << QStringLiteral("kwincompositing") << QStringLiteral("kwineffects") #ifdef KWIN_BUILD_TABBOX << QStringLiteral("kwintabbox") #endif << QStringLiteral("kwinscreenedges") << QStringLiteral("kwinscripts") ; return args; } void UserActionsMenu::init() { if (m_menu) { return; } m_menu = new QMenu; connect(m_menu, &QMenu::aboutToShow, this, &UserActionsMenu::menuAboutToShow); connect(m_menu, &QMenu::triggered, this, &UserActionsMenu::slotWindowOperation, Qt::QueuedConnection); QMenu *advancedMenu = new QMenu(m_menu); connect(advancedMenu, &QMenu::aboutToShow, [this, advancedMenu]() { advancedMenu->setPalette(m_client.data()->palette()); }); auto setShortcut = [](QAction *action, const QString &actionName) { const auto shortcuts = KGlobalAccel::self()->shortcut(Workspace::self()->findChild(actionName)); if (!shortcuts.isEmpty()) { action->setShortcut(shortcuts.first()); } }; m_moveOperation = advancedMenu->addAction(i18n("&Move")); m_moveOperation->setIcon(QIcon::fromTheme(QStringLiteral("transform-move"))); setShortcut(m_moveOperation, QStringLiteral("Window Move")); m_moveOperation->setData(Options::UnrestrictedMoveOp); m_resizeOperation = advancedMenu->addAction(i18n("&Resize")); setShortcut(m_resizeOperation, QStringLiteral("Window Resize")); m_resizeOperation->setData(Options::ResizeOp); m_keepAboveOperation = advancedMenu->addAction(i18n("Keep &Above Others")); m_keepAboveOperation->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); setShortcut(m_keepAboveOperation, QStringLiteral("Window Above Other Windows")); m_keepAboveOperation->setCheckable(true); m_keepAboveOperation->setData(Options::KeepAboveOp); m_keepBelowOperation = advancedMenu->addAction(i18n("Keep &Below Others")); m_keepBelowOperation->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); setShortcut(m_keepBelowOperation, QStringLiteral("Window Below Other Windows")); m_keepBelowOperation->setCheckable(true); m_keepBelowOperation->setData(Options::KeepBelowOp); m_fullScreenOperation = advancedMenu->addAction(i18n("&Fullscreen")); m_fullScreenOperation->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); setShortcut(m_fullScreenOperation, QStringLiteral("Window Fullscreen")); m_fullScreenOperation->setCheckable(true); m_fullScreenOperation->setData(Options::FullScreenOp); m_shadeOperation = advancedMenu->addAction(i18n("&Shade")); setShortcut(m_shadeOperation, QStringLiteral("Window Shade")); m_shadeOperation->setCheckable(true); m_shadeOperation->setData(Options::ShadeOp); m_noBorderOperation = advancedMenu->addAction(i18n("&No Border")); setShortcut(m_noBorderOperation, QStringLiteral("Window No Border")); m_noBorderOperation->setCheckable(true); m_noBorderOperation->setData(Options::NoBorderOp); advancedMenu->addSeparator(); m_shortcutOperation = advancedMenu->addAction(i18n("Window Short&cut...")); m_shortcutOperation->setIcon(QIcon::fromTheme(QStringLiteral("configure-shortcuts"))); setShortcut(m_shortcutOperation, QStringLiteral("Setup Window Shortcut")); m_shortcutOperation->setData(Options::SetupWindowShortcutOp); QAction *action = advancedMenu->addAction(i18n("Special &Window Settings...")); action->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-actions"))); action->setData(Options::WindowRulesOp); m_rulesOperation = action; action = advancedMenu->addAction(i18n("S&pecial Application Settings...")); action->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-actions"))); action->setData(Options::ApplicationRulesOp); m_applicationRulesOperation = action; if (!kwinApp()->config()->isImmutable() && !KAuthorized::authorizeControlModules(configModules(true)).isEmpty()) { advancedMenu->addSeparator(); action = advancedMenu->addAction(i18nc("Entry in context menu of window decoration to open the configuration module of KWin", "Window Manager S&ettings...")); action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); connect(action, &QAction::triggered, this, [this]() { // opens the KWin configuration QStringList args; args << QStringLiteral("--icon") << QStringLiteral("preferences-system-windows"); const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kservices5/kwinfocus.desktop")); if (!path.isEmpty()) { args << QStringLiteral("--desktopfile") << path; } args << configModules(false); QProcess *p = new Process(this); p->setArguments(args); p->setProcessEnvironment(kwinApp()->processStartupEnvironment()); p->setProgram(QStringLiteral("kcmshell5")); connect(p, static_cast(&QProcess::finished), p, &QProcess::deleteLater); connect(p, static_cast(&QProcess::error), this, [p] (QProcess::ProcessError e) { if (e == QProcess::FailedToStart) { qCDebug(KWIN_CORE) << "Failed to start kcmshell5"; } } ); p->start(); } ); } m_minimizeOperation = m_menu->addAction(i18n("Mi&nimize")); setShortcut(m_minimizeOperation, QStringLiteral("Window Minimize")); m_minimizeOperation->setData(Options::MinimizeOp); m_maximizeOperation = m_menu->addAction(i18n("Ma&ximize")); setShortcut(m_maximizeOperation, QStringLiteral("Window Maximize")); m_maximizeOperation->setCheckable(true); m_maximizeOperation->setData(Options::MaximizeOp); m_menu->addSeparator(); // Actions for window tabbing if (false) { m_removeFromTabGroup = m_menu->addAction(i18n("&Untab")); setShortcut(m_removeFromTabGroup, QStringLiteral("Untab")); m_removeFromTabGroup->setData(Options::RemoveTabFromGroupOp); m_closeTabGroup = m_menu->addAction(i18n("Close Entire &Group")); m_closeTabGroup->setIcon(QIcon::fromTheme(QStringLiteral("window-close"))); setShortcut(m_closeTabGroup, QStringLiteral("Close TabGroup")); m_closeTabGroup->setData(Options::CloseTabGroupOp); m_menu->addSeparator(); } m_menu->addSeparator(); action = m_menu->addMenu(advancedMenu); action->setText(i18n("&More Actions")); m_menu->addSeparator(); m_closeOperation = m_menu->addAction(i18n("&Close")); m_closeOperation->setIcon(QIcon::fromTheme(QStringLiteral("window-close"))); setShortcut(m_closeOperation, QStringLiteral("Window Close")); m_closeOperation->setData(Options::CloseOp); } void UserActionsMenu::discard() { delete m_menu; m_menu = NULL; m_desktopMenu = NULL; m_multipleDesktopsMenu = nullptr; m_screenMenu = NULL; m_activityMenu = NULL; m_switchToTabMenu = NULL; m_addTabsMenu = NULL; m_scriptsMenu = NULL; } void UserActionsMenu::menuAboutToShow() { if (m_client.isNull() || !m_menu) return; if (VirtualDesktopManager::self()->count() == 1) { delete m_desktopMenu; m_desktopMenu = 0; delete m_multipleDesktopsMenu; m_multipleDesktopsMenu = nullptr; } else { initDesktopPopup(); } if (screens()->count() == 1 || (!m_client.data()->isMovable() && !m_client.data()->isMovableAcrossScreens())) { delete m_screenMenu; m_screenMenu = NULL; } else { initScreenPopup(); } m_menu->setPalette(m_client.data()->palette()); m_resizeOperation->setEnabled(m_client.data()->isResizable()); m_moveOperation->setEnabled(m_client.data()->isMovableAcrossScreens()); m_maximizeOperation->setEnabled(m_client.data()->isMaximizable()); m_maximizeOperation->setChecked(m_client.data()->maximizeMode() == MaximizeFull); m_shadeOperation->setEnabled(m_client.data()->isShadeable()); m_shadeOperation->setChecked(m_client.data()->shadeMode() != ShadeNone); m_keepAboveOperation->setChecked(m_client.data()->keepAbove()); m_keepBelowOperation->setChecked(m_client.data()->keepBelow()); m_fullScreenOperation->setEnabled(m_client.data()->userCanSetFullScreen()); m_fullScreenOperation->setChecked(m_client.data()->isFullScreen()); m_noBorderOperation->setEnabled(m_client.data()->userCanSetNoBorder()); m_noBorderOperation->setChecked(m_client.data()->noBorder()); m_minimizeOperation->setEnabled(m_client.data()->isMinimizable()); m_closeOperation->setEnabled(m_client.data()->isCloseable()); m_shortcutOperation->setEnabled(m_client.data()->rules()->checkShortcut(QString()).isNull()); if (false) { initTabbingPopups(); m_addTabsMenu->setPalette(m_client.data()->palette()); } else { delete m_addTabsMenu; m_addTabsMenu = 0; } // drop the existing scripts menu delete m_scriptsMenu; m_scriptsMenu = NULL; // ask scripts whether they want to add entries for the given Client QList scriptActions = Scripting::self()->actionsForUserActionMenu(m_client.data(), m_scriptsMenu); if (!scriptActions.isEmpty()) { m_scriptsMenu = new QMenu(m_menu); m_scriptsMenu->setPalette(m_client.data()->palette()); m_scriptsMenu->addActions(scriptActions); QAction *action = m_scriptsMenu->menuAction(); // set it as the first item after desktop m_menu->insertAction(m_closeOperation, action); action->setText(i18n("&Extensions")); } m_rulesOperation->setEnabled(m_client.data()->supportsWindowRules()); m_applicationRulesOperation->setEnabled(m_client.data()->supportsWindowRules()); showHideActivityMenu(); } void UserActionsMenu::showHideActivityMenu() { #ifdef KWIN_BUILD_ACTIVITIES if (!Activities::self()) { return; } const QStringList &openActivities_ = Activities::self()->running(); qCDebug(KWIN_CORE) << "activities:" << openActivities_.size(); if (openActivities_.size() < 2) { delete m_activityMenu; m_activityMenu = 0; } else { initActivityPopup(); } #endif } void UserActionsMenu::selectPopupClientTab(QAction* action) { if (!(!m_client.isNull() && m_client.data()->tabGroup()) || !action->data().isValid()) return; if (AbstractClient *other = action->data().value()) { m_client.data()->tabGroup()->setCurrent(other); return; } // failed conversion, try "1" & "2", being prev and next int direction = action->data().toInt(); if (direction == 1) m_client.data()->tabGroup()->activatePrev(); else if (direction == 2) m_client.data()->tabGroup()->activateNext(); } static QString shortCaption(const QString &s) { if (s.length() < 64) return s; QString ss = s; return ss.replace(32,s.length()-64, QStringLiteral("...")); } void UserActionsMenu::rebuildTabListPopup() { Q_ASSERT(m_switchToTabMenu); m_switchToTabMenu->clear(); // whatever happens "0x1" and "0x2" are no heap positions ;-) m_switchToTabMenu->addAction(i18nc("Switch to tab -> Previous", "Previous"))->setData(1); m_switchToTabMenu->addAction(i18nc("Switch to tab -> Next", "Next"))->setData(2); m_switchToTabMenu->addSeparator(); for (auto i = m_client.data()->tabGroup()->clients().constBegin(), end = m_client.data()->tabGroup()->clients().constEnd(); i != end; ++i) { if ((*i)->noBorder() || *i == m_client.data()->tabGroup()->current()) continue; // cannot tab there anyway m_switchToTabMenu->addAction(shortCaption((*i)->caption()))->setData(QVariant::fromValue(*i)); } } void UserActionsMenu::entabPopupClient(QAction* action) { if (m_client.isNull() || !action->data().isValid()) return; AbstractClient *other = action->data().value(); if (!Workspace::self()->allClientList().contains(other)) // might have been lost betwenn pop-up and selection return; AbstractClient *c = m_client.data(); if (!c) { return; } c->tabBehind(other, true); if (options->focusPolicyIsReasonable()) Workspace::self()->requestFocus(c); } void UserActionsMenu::rebuildTabGroupPopup() { Q_ASSERT(m_addTabsMenu); m_addTabsMenu->clear(); const auto &clientList = Workspace::self()->allClientList(); for (auto i = clientList.constBegin(), end = clientList.constEnd(); i != end; ++i) { if (*i == m_client.data() || (*i)->noBorder()) continue; m_addTabsMenu->addAction(shortCaption((*i)->caption()))->setData(QVariant::fromValue(*i)); } if (m_addTabsMenu->actions().isEmpty()) m_addTabsMenu->addAction(i18nc("There's no window available to be attached as tab to this one", "None available"))->setEnabled(false); } void UserActionsMenu::initTabbingPopups() { bool needTabManagers = false; if (m_client.data()->tabGroup() && m_client.data()->tabGroup()->count() > 1) { needTabManagers = true; if (!m_switchToTabMenu) { m_switchToTabMenu = new QMenu(i18n("Switch to Tab"), m_menu); connect(m_switchToTabMenu, &QMenu::triggered, this, &UserActionsMenu::selectPopupClientTab); connect(m_switchToTabMenu, &QMenu::aboutToShow, this, &UserActionsMenu::rebuildTabListPopup); m_menu->insertMenu(m_removeFromTabGroup, m_switchToTabMenu); } } else { delete m_switchToTabMenu; m_switchToTabMenu = 0; } if (!m_addTabsMenu) { m_addTabsMenu = new QMenu(i18n("&Attach as tab to"), m_menu); connect(m_addTabsMenu, &QMenu::triggered, this, &UserActionsMenu::entabPopupClient); connect(m_addTabsMenu, &QMenu::aboutToShow, this, &UserActionsMenu::rebuildTabGroupPopup); m_menu->insertMenu(m_removeFromTabGroup, m_addTabsMenu); } m_addTabsMenu->menuAction()->setEnabled(!m_client.data()->isFullScreen()); m_removeFromTabGroup->setVisible(needTabManagers); m_closeTabGroup->setVisible(needTabManagers); } void UserActionsMenu::initDesktopPopup() { if (kwinApp()->operationMode() == Application::OperationModeWaylandOnly || kwinApp()->operationMode() == Application::OperationModeXwayland) { if (m_multipleDesktopsMenu) { return; } m_multipleDesktopsMenu = new QMenu(m_menu); connect(m_multipleDesktopsMenu, &QMenu::triggered, this, &UserActionsMenu::slotToggleOnVirtualDesktop); connect(m_multipleDesktopsMenu, &QMenu::aboutToShow, this, &UserActionsMenu::multipleDesktopsPopupAboutToShow); QAction *action = m_multipleDesktopsMenu->menuAction(); // set it as the first item m_menu->insertAction(m_minimizeOperation, action); action->setText(i18n("&Desktops")); } else { if (m_desktopMenu) return; m_desktopMenu = new QMenu(m_menu); connect(m_desktopMenu, &QMenu::triggered, this, &UserActionsMenu::slotSendToDesktop); connect(m_desktopMenu, &QMenu::aboutToShow, this, &UserActionsMenu::desktopPopupAboutToShow); QAction *action = m_desktopMenu->menuAction(); // set it as the first item m_menu->insertAction(m_minimizeOperation, action); action->setText(i18n("Move To &Desktop")); } } void UserActionsMenu::initScreenPopup() { if (m_screenMenu) { return; } m_screenMenu = new QMenu(m_menu); connect(m_screenMenu, &QMenu::triggered, this, &UserActionsMenu::slotSendToScreen); connect(m_screenMenu, &QMenu::aboutToShow, this, &UserActionsMenu::screenPopupAboutToShow); QAction *action = m_screenMenu->menuAction(); // set it as the first item after desktop m_menu->insertAction(m_activityMenu ? m_activityMenu->menuAction() : m_minimizeOperation, action); action->setText(i18n("Move To &Screen")); } void UserActionsMenu::initActivityPopup() { if (m_activityMenu) return; m_activityMenu = new QMenu(m_menu); connect(m_activityMenu, &QMenu::triggered, this, &UserActionsMenu::slotToggleOnActivity); connect(m_activityMenu, &QMenu::aboutToShow, this, &UserActionsMenu::activityPopupAboutToShow); QAction *action = m_activityMenu->menuAction(); // set it as the first item m_menu->insertAction(m_minimizeOperation, action); action->setText(i18n("Ac&tivities")); //FIXME is that a good string? } void UserActionsMenu::desktopPopupAboutToShow() { if (!m_desktopMenu) return; const VirtualDesktopManager *vds = VirtualDesktopManager::self(); m_desktopMenu->clear(); m_desktopMenu->setPalette(m_client.data()->palette()); QActionGroup *group = new QActionGroup(m_desktopMenu); QAction *action = m_desktopMenu->addAction(i18n("&All Desktops")); action->setData(0); action->setCheckable(true); group->addAction(action); if (!m_client.isNull() && m_client.data()->isOnAllDesktops()) action->setChecked(true); m_desktopMenu->addSeparator(); const uint BASE = 10; for (uint i = 1; i <= vds->count(); ++i) { QString basic_name(QStringLiteral("%1 %2")); if (i < BASE) { basic_name.prepend(QLatin1Char('&')); } action = m_desktopMenu->addAction(basic_name.arg(i).arg(vds->name(i).replace(QLatin1Char('&'), QStringLiteral("&&")))); action->setData(i); action->setCheckable(true); group->addAction(action); if (!m_client.isNull() && !m_client.data()->isOnAllDesktops() && m_client.data()->isOnDesktop(i)) action->setChecked(true); } m_desktopMenu->addSeparator(); action = m_desktopMenu->addAction(i18nc("Create a new desktop and move there the window", "&New Desktop")); action->setData(vds->count() + 1); if (vds->count() >= vds->maximum()) action->setEnabled(false); } void UserActionsMenu::multipleDesktopsPopupAboutToShow() { if (!m_multipleDesktopsMenu) return; const VirtualDesktopManager *vds = VirtualDesktopManager::self(); m_multipleDesktopsMenu->clear(); m_multipleDesktopsMenu->setPalette(m_client.data()->palette()); QAction *action = m_multipleDesktopsMenu->addAction(i18n("&All Desktops")); action->setData(0); action->setCheckable(true); QActionGroup *allDesktopsGroup = new QActionGroup(m_multipleDesktopsMenu); allDesktopsGroup->addAction(action); if (!m_client.isNull() && m_client.data()->isOnAllDesktops()) { action->setChecked(true); } m_multipleDesktopsMenu->addSeparator(); const uint BASE = 10; for (uint i = 1; i <= vds->count(); ++i) { QString basic_name(QStringLiteral("%1 %2")); if (i < BASE) { basic_name.prepend(QLatin1Char('&')); } QWidgetAction *action = new QWidgetAction(m_multipleDesktopsMenu); QCheckBox *box = new QCheckBox(basic_name.arg(i).arg(vds->name(i).replace(QLatin1Char('&'), QStringLiteral("&&"))), m_multipleDesktopsMenu); action->setDefaultWidget(box); box->setBackgroundRole(m_multipleDesktopsMenu->backgroundRole()); box->setForegroundRole(m_multipleDesktopsMenu->foregroundRole()); box->setPalette(m_multipleDesktopsMenu->palette()); connect(box, &QCheckBox::clicked, action, &QAction::triggered); m_multipleDesktopsMenu->addAction(action); action->setData(i); if (!m_client.isNull() && !m_client.data()->isOnAllDesktops() && m_client.data()->isOnDesktop(i)) { box->setChecked(true); } } m_multipleDesktopsMenu->addSeparator(); action = m_multipleDesktopsMenu->addAction(i18nc("Create a new desktop and move there the window", "&New Desktop")); action->setData(vds->count() + 1); if (vds->count() >= vds->maximum()) action->setEnabled(false); } void UserActionsMenu::screenPopupAboutToShow() { if (!m_screenMenu) { return; } m_screenMenu->clear(); m_screenMenu->setPalette(m_client.data()->palette()); QActionGroup *group = new QActionGroup(m_screenMenu); for (int i = 0; icount(); ++i) { // assumption: there are not more than 9 screens attached. QAction *action = m_screenMenu->addAction(i18nc("@item:inmenu List of all Screens to send a window to. First argument is a number, second the output identifier. E.g. Screen 1 (HDMI1)", "Screen &%1 (%2)", (i+1), screens()->name(i))); action->setData(i); action->setCheckable(true); if (!m_client.isNull() && i == m_client.data()->screen()) { action->setChecked(true); } group->addAction(action); } } void UserActionsMenu::activityPopupAboutToShow() { if (!m_activityMenu) return; #ifdef KWIN_BUILD_ACTIVITIES if (!Activities::self()) { return; } m_activityMenu->clear(); m_activityMenu->setPalette(m_client.data()->palette()); QAction *action = m_activityMenu->addAction(i18n("&All Activities")); action->setData(QString()); action->setCheckable(true); static QPointer allActivitiesGroup; if (!allActivitiesGroup) { allActivitiesGroup = new QActionGroup(m_activityMenu); } allActivitiesGroup->addAction(action); if (!m_client.isNull() && m_client.data()->isOnAllActivities()) action->setChecked(true); m_activityMenu->addSeparator(); foreach (const QString &id, Activities::self()->running()) { KActivities::Info activity(id); QString name = activity.name(); name.replace('&', "&&"); QWidgetAction *action = new QWidgetAction(m_activityMenu); QCheckBox *box = new QCheckBox(name, m_activityMenu); action->setDefaultWidget(box); const QString icon = activity.icon(); if (!icon.isEmpty()) box->setIcon(QIcon::fromTheme(icon)); box->setBackgroundRole(m_activityMenu->backgroundRole()); box->setForegroundRole(m_activityMenu->foregroundRole()); box->setPalette(m_activityMenu->palette()); connect(box, &QCheckBox::clicked, action, &QAction::triggered); m_activityMenu->addAction(action); action->setData(id); if (!m_client.isNull() && !m_client.data()->isOnAllActivities() && m_client.data()->isOnActivity(id)) box->setChecked(true); } #endif } void UserActionsMenu::slotWindowOperation(QAction *action) { if (!action->data().isValid()) return; Options::WindowOperation op = static_cast< Options::WindowOperation >(action->data().toInt()); QWeakPointer c = (!m_client.isNull()) ? m_client : QWeakPointer(Workspace::self()->activeClient()); if (c.isNull()) return; QString type; switch(op) { case Options::FullScreenOp: if (!c.data()->isFullScreen() && c.data()->userCanSetFullScreen()) type = QStringLiteral("fullscreenaltf3"); break; case Options::NoBorderOp: if (!c.data()->noBorder() && c.data()->userCanSetNoBorder()) type = QStringLiteral("noborderaltf3"); break; default: break; }; if (!type.isEmpty()) helperDialog(type, c); // need to delay performing the window operation as we need to have the // user actions menu closed before we destroy the decoration. Otherwise Qt crashes qRegisterMetaType(); QMetaObject::invokeMethod(workspace(), "performWindowOperation", Qt::QueuedConnection, Q_ARG(KWin::AbstractClient*, c.data()), Q_ARG(Options::WindowOperation, op)); } void UserActionsMenu::slotSendToDesktop(QAction *action) { bool ok = false; uint desk = action->data().toUInt(&ok); if (!ok) { return; } if (m_client.isNull()) return; Workspace *ws = Workspace::self(); VirtualDesktopManager *vds = VirtualDesktopManager::self(); if (desk == 0) { // the 'on_all_desktops' menu entry m_client.data()->setOnAllDesktops(!m_client.data()->isOnAllDesktops()); return; } else if (desk > vds->count()) { vds->setCount(desk); } ws->sendClientToDesktop(m_client.data(), desk, false); } void UserActionsMenu::slotToggleOnVirtualDesktop(QAction *action) { bool ok = false; uint desk = action->data().toUInt(&ok); if (!ok) { return; } if (m_client.isNull()) { return; } VirtualDesktopManager *vds = VirtualDesktopManager::self(); if (desk == 0) { // the 'on_all_desktops' menu entry m_client.data()->setOnAllDesktops(!m_client.data()->isOnAllDesktops()); return; } else if (desk > vds->count()) { vds->setCount(desk); } VirtualDesktop *virtualDesktop = VirtualDesktopManager::self()->desktopForX11Id(desk); if (m_client.data()->desktops().contains(virtualDesktop)) { m_client.data()->leaveDesktop(virtualDesktop); } else { m_client.data()->enterDesktop(virtualDesktop); } } void UserActionsMenu::slotSendToScreen(QAction *action) { const int screen = action->data().toInt(); if (m_client.isNull()) { return; } if (screen >= screens()->count()) { return; } Workspace::self()->sendClientToScreen(m_client.data(), screen); } void UserActionsMenu::slotToggleOnActivity(QAction *action) { #ifdef KWIN_BUILD_ACTIVITIES if (!Activities::self()) { return; } QString activity = action->data().toString(); if (m_client.isNull()) return; if (activity.isEmpty()) { // the 'on_all_activities' menu entry m_client.data()->setOnAllActivities(!m_client.data()->isOnAllActivities()); return; } Client *c = dynamic_cast(m_client.data()); if (!c) { return; } Activities::self()->toggleClientOnActivity(c, activity, false); if (m_activityMenu && m_activityMenu->isVisible() && m_activityMenu->actions().count()) { const bool isOnAll = m_client.data()->isOnAllActivities(); m_activityMenu->actions().at(0)->setChecked(isOnAll); if (isOnAll) { // toggleClientOnActivity interprets "on all" as "on none" and // susequent toggling ("off") would move the client to only that activity. // bug #330838 -> set all but "on all" off to "force proper usage" for (int i = 1; i < m_activityMenu->actions().count(); ++i) { if (QWidgetAction *qwa = qobject_cast(m_activityMenu->actions().at(i))) { if (QCheckBox *qcb = qobject_cast(qwa->defaultWidget())) { qcb->setChecked(false); } } } } } #else Q_UNUSED(action) #endif } //**************************************** // ShortcutDialog //**************************************** ShortcutDialog::ShortcutDialog(const QKeySequence& cut) : _shortcut(cut) { m_ui.setupUi(this); m_ui.keySequenceEdit->setKeySequence(cut); m_ui.warning->hide(); // Listen to changed shortcuts connect(m_ui.keySequenceEdit, &QKeySequenceEdit::editingFinished, this, &ShortcutDialog::keySequenceChanged); connect(m_ui.clearButton, &QToolButton::clicked, [this]{ _shortcut = QKeySequence(); }); m_ui.keySequenceEdit->setFocus(); setWindowFlags(Qt::Popup | Qt::X11BypassWindowManagerHint); } void ShortcutDialog::accept() { QKeySequence seq = shortcut(); if (!seq.isEmpty()) { if (seq[0] == Qt::Key_Escape) { reject(); return; } if (seq[0] == Qt::Key_Space || (seq[0] & Qt::KeyboardModifierMask) == 0) { // clear m_ui.keySequenceEdit->clear(); QDialog::accept(); return; } } QDialog::accept(); } void ShortcutDialog::done(int r) { QDialog::done(r); emit dialogDone(r == Accepted); } void ShortcutDialog::keySequenceChanged() { activateWindow(); // where is the kbd focus lost? cause of popup state? QKeySequence seq = m_ui.keySequenceEdit->keySequence(); if (_shortcut == seq) return; // don't try to update the same if (seq.isEmpty()) { // clear _shortcut = seq; return; } if (seq.count() > 1) { seq = QKeySequence(seq[0]); m_ui.keySequenceEdit->setKeySequence(seq); } // Check if the key sequence is used currently QString sc = seq.toString(); // NOTICE - seq.toString() & the entries in "conflicting" randomly get invalidated after the next call (if no sc has been set & conflicting isn't empty?!) QList conflicting = KGlobalAccel::getGlobalShortcutsByKey(seq); if (!conflicting.isEmpty()) { const KGlobalShortcutInfo &conflict = conflicting.at(0); m_ui.warning->setText(i18nc("'%1' is a keyboard shortcut like 'ctrl+w'", "%1 is already in use", sc)); m_ui.warning->setToolTip(i18nc("keyboard shortcut '%1' is used by action '%2' in application '%3'", "%1 is used by %2 in %3", sc, conflict.friendlyName(), conflict.componentFriendlyName())); m_ui.warning->show(); m_ui.keySequenceEdit->setKeySequence(shortcut()); } else if (seq != _shortcut) { m_ui.warning->hide(); if (QPushButton *ok = m_ui.buttonBox->button(QDialogButtonBox::Ok)) ok->setFocus(); } _shortcut = seq; } QKeySequence ShortcutDialog::shortcut() const { return _shortcut; } //**************************************** // Workspace //**************************************** void Workspace::slotIncreaseWindowOpacity() { if (!active_client) { return; } active_client->setOpacity(qMin(active_client->opacity() + 0.05, 1.0)); } void Workspace::slotLowerWindowOpacity() { if (!active_client) { return; } active_client->setOpacity(qMax(active_client->opacity() - 0.05, 0.05)); } void Workspace::closeActivePopup() { if (active_popup) { active_popup->close(); active_popup = NULL; active_popup_client = NULL; } m_userActionsMenu->close(); } template void Workspace::initShortcut(const QString &actionName, const QString &description, const QKeySequence &shortcut, Slot slot, const QVariant &data) { initShortcut(actionName, description, shortcut, this, slot, data); } template void Workspace::initShortcut(const QString &actionName, const QString &description, const QKeySequence &shortcut, T *receiver, Slot slot, const QVariant &data) { QAction *a = new QAction(this); a->setProperty("componentName", QStringLiteral(KWIN_NAME)); a->setObjectName(actionName); a->setText(description); if (data.isValid()) { a->setData(data); } KGlobalAccel::self()->setDefaultShortcut(a, QList() << shortcut); KGlobalAccel::self()->setShortcut(a, QList() << shortcut); input()->registerShortcut(shortcut, a, receiver, slot); } /** * Creates the global accel object \c keys. **/ void Workspace::initShortcuts() { #define IN_KWIN #include "kwinbindings.cpp" #ifdef KWIN_BUILD_TABBOX TabBox::TabBox::self()->initShortcuts(); #endif VirtualDesktopManager::self()->initShortcuts(); + kwinApp()->platform()->colorCorrectManager()->initShortcuts(); m_userActionsMenu->discard(); // so that it's recreated next time } void Workspace::setupWindowShortcut(AbstractClient* c) { assert(client_keys_dialog == NULL); // TODO: PORT ME (KGlobalAccel related) //keys->setEnabled( false ); //disable_shortcuts_keys->setEnabled( false ); //client_keys->setEnabled( false ); client_keys_dialog = new ShortcutDialog(c->shortcut()); client_keys_client = c; connect(client_keys_dialog, &ShortcutDialog::dialogDone, this, &Workspace::setupWindowShortcutDone); QRect r = clientArea(ScreenArea, c); QSize size = client_keys_dialog->sizeHint(); QPoint pos = c->pos() + c->clientPos(); if (pos.x() + size.width() >= r.right()) pos.setX(r.right() - size.width()); if (pos.y() + size.height() >= r.bottom()) pos.setY(r.bottom() - size.height()); client_keys_dialog->move(pos); client_keys_dialog->show(); active_popup = client_keys_dialog; active_popup_client = c; } void Workspace::setupWindowShortcutDone(bool ok) { // keys->setEnabled( true ); // disable_shortcuts_keys->setEnabled( true ); // client_keys->setEnabled( true ); if (ok) client_keys_client->setShortcut(client_keys_dialog->shortcut().toString()); closeActivePopup(); client_keys_dialog->deleteLater(); client_keys_dialog = NULL; client_keys_client = NULL; if (active_client) active_client->takeFocus(); } void Workspace::clientShortcutUpdated(AbstractClient* c) { QString key = QStringLiteral("_k_session:%1").arg(c->window()); QAction* action = findChild(key); if (!c->shortcut().isEmpty()) { if (action == NULL) { // new shortcut action = new QAction(this); kwinApp()->platform()->setupActionForGlobalAccel(action); action->setProperty("componentName", QStringLiteral(KWIN_NAME)); action->setObjectName(key); action->setText(i18n("Activate Window (%1)", c->caption())); connect(action, &QAction::triggered, c, std::bind(&Workspace::activateClient, this, c, true)); } // no autoloading, since it's configured explicitly here and is not meant to be reused // (the key is the window id anyway, which is kind of random) KGlobalAccel::self()->setShortcut(action, QList() << c->shortcut(), KGlobalAccel::NoAutoloading); action->setEnabled(true); } else { KGlobalAccel::self()->removeAllShortcuts(action); delete action; } } void Workspace::performWindowOperation(AbstractClient* c, Options::WindowOperation op) { if (!c) return; if (op == Options::MoveOp || op == Options::UnrestrictedMoveOp) Cursor::setPos(c->geometry().center()); if (op == Options::ResizeOp || op == Options::UnrestrictedResizeOp) Cursor::setPos(c->geometry().bottomRight()); switch(op) { case Options::MoveOp: c->performMouseCommand(Options::MouseMove, Cursor::pos()); break; case Options::UnrestrictedMoveOp: c->performMouseCommand(Options::MouseUnrestrictedMove, Cursor::pos()); break; case Options::ResizeOp: c->performMouseCommand(Options::MouseResize, Cursor::pos()); break; case Options::UnrestrictedResizeOp: c->performMouseCommand(Options::MouseUnrestrictedResize, Cursor::pos()); break; case Options::CloseOp: QMetaObject::invokeMethod(c, "closeWindow", Qt::QueuedConnection); break; case Options::MaximizeOp: c->maximize(c->maximizeMode() == MaximizeFull ? MaximizeRestore : MaximizeFull); break; case Options::HMaximizeOp: c->maximize(c->maximizeMode() ^ MaximizeHorizontal); break; case Options::VMaximizeOp: c->maximize(c->maximizeMode() ^ MaximizeVertical); break; case Options::RestoreOp: c->maximize(MaximizeRestore); break; case Options::MinimizeOp: c->minimize(); break; case Options::ShadeOp: c->performMouseCommand(Options::MouseShade, Cursor::pos()); break; case Options::OnAllDesktopsOp: c->setOnAllDesktops(!c->isOnAllDesktops()); break; case Options::FullScreenOp: c->setFullScreen(!c->isFullScreen(), true); break; case Options::NoBorderOp: c->setNoBorder(!c->noBorder()); break; case Options::KeepAboveOp: { StackingUpdatesBlocker blocker(this); bool was = c->keepAbove(); c->setKeepAbove(!c->keepAbove()); if (was && !c->keepAbove()) raiseClient(c); break; } case Options::KeepBelowOp: { StackingUpdatesBlocker blocker(this); bool was = c->keepBelow(); c->setKeepBelow(!c->keepBelow()); if (was && !c->keepBelow()) lowerClient(c); break; } case Options::OperationsOp: c->performMouseCommand(Options::MouseShade, Cursor::pos()); break; case Options::WindowRulesOp: RuleBook::self()->edit(c, false); break; case Options::ApplicationRulesOp: RuleBook::self()->edit(c, true); break; case Options::SetupWindowShortcutOp: setupWindowShortcut(c); break; case Options::LowerOp: lowerClient(c); break; case Options::TabDragOp: // Handled by decoration itself case Options::NoOp: break; case Options::RemoveTabFromGroupOp: if (c->untab(c->geometry().translated(cascadeOffset(c))) && options->focusPolicyIsReasonable()) takeActivity(c, ActivityFocus | ActivityRaise); break; case Options::ActivateNextTabOp: if (c->tabGroup()) c->tabGroup()->activateNext(); break; case Options::ActivatePreviousTabOp: if (c->tabGroup()) c->tabGroup()->activatePrev(); break; case Options::CloseTabGroupOp: c->tabGroup()->closeAll(); break; } } /** * Called by the decoration in the new API to determine what buttons the user has configured for * window tab dragging and the operations menu. **/ Options::WindowOperation Client::mouseButtonToWindowOperation(Qt::MouseButtons button) { Options::MouseCommand com = Options::MouseNothing; bool active = isActive(); if (!wantsInput()) // we cannot be active, use it anyway active = true; if (button == Qt::LeftButton) com = active ? options->commandActiveTitlebar1() : options->commandInactiveTitlebar1(); else if (button == Qt::MidButton) com = active ? options->commandActiveTitlebar2() : options->commandInactiveTitlebar2(); else if (button == Qt::RightButton) com = active ? options->commandActiveTitlebar3() : options->commandInactiveTitlebar3(); // TODO: Complete the list if (com == Options::MouseDragTab) return Options::TabDragOp; if (com == Options::MouseOperationsMenu) return Options::OperationsOp; return Options::NoOp; } /** * Performs a mouse command on this client (see options.h) **/ bool Client::performMouseCommand(Options::MouseCommand command, const QPoint &globalPos) { bool replay = false; switch(command) { case Options::MouseShade : toggleShade(); cancelShadeHoverTimer(); break; case Options::MouseSetShade: setShade(ShadeNormal); cancelShadeHoverTimer(); break; case Options::MouseUnsetShade: setShade(ShadeNone); cancelShadeHoverTimer(); break; default: return AbstractClient::performMouseCommand(command, globalPos); } return replay; } void Workspace::slotActivateAttentionWindow() { if (attention_chain.count() > 0) activateClient(attention_chain.first()); } static uint senderValue(QObject *sender) { QAction *act = qobject_cast(sender); bool ok = false; uint i = -1; if (act) i = act->data().toUInt(&ok); if (ok) return i; return -1; } #define USABLE_ACTIVE_CLIENT (active_client && !(active_client->isDesktop() || active_client->isDock())) void Workspace::slotWindowToDesktop(uint i) { if (USABLE_ACTIVE_CLIENT) { if (i < 1) return; if (i >= 1 && i <= VirtualDesktopManager::self()->count()) sendClientToDesktop(active_client, i, true); } } static bool screenSwitchImpossible() { if (!screens()->isCurrentFollowsMouse()) return false; QStringList args; args << QStringLiteral("--passivepopup") << i18n("The window manager is configured to consider the screen with the mouse on it as active one.\n" "Therefore it is not possible to switch to a screen explicitly.") << QStringLiteral("20"); KProcess::startDetached(QStringLiteral("kdialog"), args); return true; } void Workspace::slotSwitchToScreen() { if (screenSwitchImpossible()) return; const int i = senderValue(sender()); if (i > -1) setCurrentScreen(i); } void Workspace::slotSwitchToNextScreen() { if (screenSwitchImpossible()) return; setCurrentScreen((screens()->current() + 1) % screens()->count()); } void Workspace::slotSwitchToPrevScreen() { if (screenSwitchImpossible()) return; setCurrentScreen((screens()->current() + screens()->count() - 1) % screens()->count()); } void Workspace::slotWindowToScreen() { if (USABLE_ACTIVE_CLIENT) { const int i = senderValue(sender()); if (i < 0) return; if (i >= 0 && i <= screens()->count()) { sendClientToScreen(active_client, i); } } } void Workspace::slotWindowToNextScreen() { if (USABLE_ACTIVE_CLIENT) sendClientToScreen(active_client, (active_client->screen() + 1) % screens()->count()); } void Workspace::slotWindowToPrevScreen() { if (USABLE_ACTIVE_CLIENT) sendClientToScreen(active_client, (active_client->screen() + screens()->count() - 1) % screens()->count()); } /** * Maximizes the active client. **/ void Workspace::slotWindowMaximize() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::MaximizeOp); } /** * Maximizes the active client vertically. **/ void Workspace::slotWindowMaximizeVertical() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::VMaximizeOp); } /** * Maximizes the active client horiozontally. **/ void Workspace::slotWindowMaximizeHorizontal() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::HMaximizeOp); } /** * Minimizes the active client. **/ void Workspace::slotWindowMinimize() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::MinimizeOp); } /** * Shades/unshades the active client respectively. **/ void Workspace::slotWindowShade() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::ShadeOp); } /** * Raises the active client. **/ void Workspace::slotWindowRaise() { if (USABLE_ACTIVE_CLIENT) raiseClient(active_client); } /** * Lowers the active client. **/ void Workspace::slotWindowLower() { if (USABLE_ACTIVE_CLIENT) { lowerClient(active_client); // As this most likely makes the window no longer visible change the // keyboard focus to the next available window. //activateNextClient( c ); // Doesn't work when we lower a child window if (active_client->isActive() && options->focusPolicyIsReasonable()) { if (options->isNextFocusPrefersMouse()) { AbstractClient *next = clientUnderMouse(active_client->screen()); if (next && next != active_client) requestFocus(next, false); } else { activateClient(topClientOnDesktop(VirtualDesktopManager::self()->current(), -1)); } } } } /** * Does a toggle-raise-and-lower on the active client. **/ void Workspace::slotWindowRaiseOrLower() { if (USABLE_ACTIVE_CLIENT) raiseOrLowerClient(active_client); } void Workspace::slotWindowOnAllDesktops() { if (USABLE_ACTIVE_CLIENT) active_client->setOnAllDesktops(!active_client->isOnAllDesktops()); } void Workspace::slotWindowFullScreen() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::FullScreenOp); } void Workspace::slotWindowNoBorder() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::NoBorderOp); } void Workspace::slotWindowAbove() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::KeepAboveOp); } void Workspace::slotWindowBelow() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::KeepBelowOp); } void Workspace::slotSetupWindowShortcut() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::SetupWindowShortcutOp); } /** * Toggles show desktop. **/ void Workspace::slotToggleShowDesktop() { setShowingDesktop(!showingDesktop()); } template void windowToDesktop(AbstractClient *c) { VirtualDesktopManager *vds = VirtualDesktopManager::self(); Workspace *ws = Workspace::self(); Direction functor; // TODO: why is options->isRollOverDesktops() not honored? const auto desktop = functor(nullptr, true); if (c && !c->isDesktop() && !c->isDock()) { ws->setMoveResizeClient(c); vds->setCurrent(desktop); ws->setMoveResizeClient(nullptr); } } /** * Moves the active client to the next desktop. **/ void Workspace::slotWindowToNextDesktop() { if (USABLE_ACTIVE_CLIENT) windowToNextDesktop(active_client); } void Workspace::windowToNextDesktop(AbstractClient* c) { windowToDesktop(c); } /** * Moves the active client to the previous desktop. **/ void Workspace::slotWindowToPreviousDesktop() { if (USABLE_ACTIVE_CLIENT) windowToPreviousDesktop(active_client); } void Workspace::windowToPreviousDesktop(AbstractClient* c) { windowToDesktop(c); } template void activeClientToDesktop() { VirtualDesktopManager *vds = VirtualDesktopManager::self(); Workspace *ws = Workspace::self(); const int current = vds->current(); Direction functor; const int d = functor(current, options->isRollOverDesktops()); if (d == current) { return; } ws->setMoveResizeClient(ws->activeClient()); vds->setCurrent(d); ws->setMoveResizeClient(nullptr); } void Workspace::slotWindowToDesktopRight() { if (USABLE_ACTIVE_CLIENT) { activeClientToDesktop(); } } void Workspace::slotWindowToDesktopLeft() { if (USABLE_ACTIVE_CLIENT) { activeClientToDesktop(); } } void Workspace::slotWindowToDesktopUp() { if (USABLE_ACTIVE_CLIENT) { activeClientToDesktop(); } } void Workspace::slotWindowToDesktopDown() { if (USABLE_ACTIVE_CLIENT) { activeClientToDesktop(); } } void Workspace::slotActivateNextTab() { if (active_client && active_client->tabGroup()) active_client->tabGroup()->activateNext(); } void Workspace::slotActivatePrevTab() { if (active_client && active_client->tabGroup()) active_client->tabGroup()->activatePrev(); } void Workspace::slotUntab() { if (active_client) active_client->untab(active_client->geometry().translated(cascadeOffset(active_client))); } /** * Kill Window feature, similar to xkill. **/ void Workspace::slotKillWindow() { if (m_windowKiller.isNull()) { m_windowKiller.reset(new KillWindow()); } m_windowKiller->start(); } /** * Switches to the nearest window in given direction. **/ void Workspace::switchWindow(Direction direction) { if (!active_client) return; AbstractClient *c = active_client; int desktopNumber = c->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : c->desktop(); // Centre of the active window QPoint curPos(c->pos().x() + c->geometry().width() / 2, c->pos().y() + c->geometry().height() / 2); if (!switchWindow(c, direction, curPos, desktopNumber)) { auto opposite = [&] { switch(direction) { case DirectionNorth: return QPoint(curPos.x(), screens()->geometry().height()); case DirectionSouth: return QPoint(curPos.x(), 0); case DirectionEast: return QPoint(0, curPos.y()); case DirectionWest: return QPoint(screens()->geometry().width(), curPos.y()); default: Q_UNREACHABLE(); } }; switchWindow(c, direction, opposite(), desktopNumber); } } bool Workspace::switchWindow(AbstractClient *c, Direction direction, QPoint curPos, int d) { AbstractClient *switchTo = nullptr; int bestScore = 0; ToplevelList clist = stackingOrder(); for (ToplevelList::Iterator i = clist.begin(); i != clist.end(); ++i) { auto client = qobject_cast(*i); if (!client) { continue; } if (client->wantsTabFocus() && *i != c && client->isOnDesktop(d) && !client->isMinimized() && (*i)->isOnCurrentActivity()) { // Centre of the other window QPoint other(client->pos().x() + client->geometry().width() / 2, client->pos().y() + client->geometry().height() / 2); int distance; int offset; switch(direction) { case DirectionNorth: distance = curPos.y() - other.y(); offset = qAbs(other.x() - curPos.x()); break; case DirectionEast: distance = other.x() - curPos.x(); offset = qAbs(other.y() - curPos.y()); break; case DirectionSouth: distance = other.y() - curPos.y(); offset = qAbs(other.x() - curPos.x()); break; case DirectionWest: distance = curPos.x() - other.x(); offset = qAbs(other.y() - curPos.y()); break; default: distance = -1; offset = -1; } if (distance > 0) { // Inverse score int score = distance + offset + ((offset * offset) / distance); if (score < bestScore || !switchTo) { switchTo = client; bestScore = score; } } } } if (switchTo) { if (switchTo->tabGroup()) switchTo = switchTo->tabGroup()->current(); activateClient(switchTo); } return switchTo; } /** * Shows the window operations popup menu for the active client. **/ void Workspace::slotWindowOperations() { if (!active_client) return; QPoint pos = active_client->pos() + active_client->clientPos(); showWindowMenu(QRect(pos, pos), active_client); } void Workspace::showWindowMenu(const QRect &pos, AbstractClient* cl) { m_userActionsMenu->show(pos, cl); } void Workspace::showApplicationMenu(const QRect &pos, AbstractClient *c, int actionId) { ApplicationMenu::self()->showApplicationMenu(c->geometry().topLeft() + pos.bottomLeft(), c, actionId); } /** * Closes the active client. **/ void Workspace::slotWindowClose() { // TODO: why? // if ( tab_box->isVisible()) // return; if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::CloseOp); } /** * Starts keyboard move mode for the active client. **/ void Workspace::slotWindowMove() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::UnrestrictedMoveOp); } /** * Starts keyboard resize mode for the active client. **/ void Workspace::slotWindowResize() { if (USABLE_ACTIVE_CLIENT) performWindowOperation(active_client, Options::UnrestrictedResizeOp); } #undef USABLE_ACTIVE_CLIENT void AbstractClient::setShortcut(const QString& _cut) { QString cut = rules()->checkShortcut(_cut); auto updateShortcut = [this](const QKeySequence &cut = QKeySequence()) { if (_shortcut == cut) return; _shortcut = cut; setShortcutInternal(); }; if (cut.isEmpty()) { updateShortcut(); return; } if (cut == shortcut().toString()) { return; // no change } // Format: // base+(abcdef)base+(abcdef) // E.g. Alt+Ctrl+(ABCDEF);Meta+X,Meta+(ABCDEF) if (!cut.contains(QLatin1Char('(')) && !cut.contains(QLatin1Char(')')) && !cut.contains(QLatin1String(" - "))) { if (workspace()->shortcutAvailable(cut, this)) updateShortcut(QKeySequence(cut)); else updateShortcut(); return; } QList< QKeySequence > keys; QStringList groups = cut.split(QStringLiteral(" - ")); for (QStringList::ConstIterator it = groups.constBegin(); it != groups.constEnd(); ++it) { QRegExp reg(QStringLiteral("(.*\\+)\\((.*)\\)")); if (reg.indexIn(*it) > -1) { QString base = reg.cap(1); QString list = reg.cap(2); for (int i = 0; i < list.length(); ++i) { QKeySequence c(base + list[ i ]); if (!c.isEmpty()) keys.append(c); } } else { // regexp doesn't match, so it should be a normal shortcut QKeySequence c(*it); if (!c.isEmpty()) { keys.append(c); } } } for (auto it = keys.constBegin(); it != keys.constEnd(); ++it) { if (_shortcut == *it) // current one is in the list return; } for (auto it = keys.constBegin(); it != keys.constEnd(); ++it) { if (workspace()->shortcutAvailable(*it, this)) { updateShortcut(*it); return; } } updateShortcut(); } void AbstractClient::setShortcutInternal() { updateCaption(); workspace()->clientShortcutUpdated(this); } void Client::setShortcutInternal() { updateCaption(); #if 0 workspace()->clientShortcutUpdated(this); #else // Workaround for kwin<->kglobalaccel deadlock, when KWin has X grab and the kded // kglobalaccel module tries to create the key grab. KWin should preferably grab // they keys itself anyway :(. QTimer::singleShot(0, this, std::bind(&Workspace::clientShortcutUpdated, workspace(), this)); #endif } bool Workspace::shortcutAvailable(const QKeySequence &cut, AbstractClient* ignore) const { if (ignore && cut == ignore->shortcut()) return true; if (!KGlobalAccel::getGlobalShortcutsByKey(cut).isEmpty()) { return false; } for (auto it = m_allClients.constBegin(); it != m_allClients.constEnd(); ++it) { if ((*it) != ignore && (*it)->shortcut() == cut) return false; } return true; } } // namespace