| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "remoting/host/linux/gnome_desktop_resizer.h" |
| |
| #include <functional> |
| #include <optional> |
| |
| #include "base/check.h" |
| #include "base/containers/flat_set.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/sequence_checker.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/types/expected.h" |
| #include "remoting/base/constants.h" |
| #include "remoting/base/logging.h" |
| #include "remoting/host/base/screen_resolution.h" |
| #include "remoting/host/linux/gnome_display_config.h" |
| #include "remoting/host/linux/gnome_interaction_strategy.h" |
| #include "remoting/host/linux/pipewire_capture_stream.h" |
| #include "remoting/host/linux/pipewire_capture_stream_manager.h" |
| #include "remoting/proto/control.pb.h" |
| #include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h" |
| #include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h" |
| #include "ui/base/glib/gsettings.h" |
| |
| namespace remoting { |
| |
| namespace { |
| |
| // Logical layout mode is preferred since it has better user experience with a |
| // mixed-DPI setup. |
| constexpr GnomeDisplayConfig::LayoutMode kPreferredLayoutMode = |
| GnomeDisplayConfig::LayoutMode::kLogical; |
| |
| constexpr base::TimeDelta kClearPreferredConfigDelay = base::Seconds(5); |
| |
| inline double InverseIfLessThanOne(double v) { |
| return v < 1.0 ? 1.0 / v : v; |
| } |
| |
| // Pick the scale that is proportionally closest to `preferred_scale`. For |
| // example, the best scale for 1.5 with supported_scales=[1, 2] is 2, since |
| // 2 / 1.5 = 1.33, which is smaller than 1.5 / 1 = 1.5. |
| inline double FindBestScale(double preferred_scale, |
| const std::vector<double>& supported_scales, |
| bool ignore_fractional_scales) { |
| DCHECK_GT(preferred_scale, 0.0); |
| auto it = std::ranges::min_element( |
| supported_scales, |
| [preferred_scale, ignore_fractional_scales](double s1, double s2) { |
| DCHECK_GT(s1, 0.0); |
| DCHECK_GT(s2, 0.0); |
| if (ignore_fractional_scales && trunc(s1) == s1 && trunc(s2) != s2) { |
| // Make non-fractional scales better than fractional scales. |
| return true; |
| } |
| return InverseIfLessThanOne(preferred_scale / s1) < |
| InverseIfLessThanOne(preferred_scale / s2); |
| }); |
| if (it == supported_scales.end()) { |
| LOG(ERROR) << "Cannot find best scale for " << preferred_scale; |
| return 1.0; |
| } |
| if (ignore_fractional_scales && trunc(*it) != *it) { |
| LOG(ERROR) << "Cannot find non-fractional scales"; |
| return 1.0; |
| } |
| return *it; |
| } |
| |
| inline bool IsSameScale(double s1, double s2) { |
| return std::abs(s1 - s2) < 0.01; |
| } |
| |
| // Note: this method only adds a monitor for the purpose of layout calculation. |
| // DO NOT call ApplyMonitorsConfig with the updated `config`. |
| void AddMonitorForLayoutCalculation(GnomeDisplayConfig& config, |
| const webrtc::DesktopVector& position, |
| const ScreenResolution& resolution) { |
| // We can't use the screen_id as the key, since it may be empty. |
| GnomeDisplayConfig::MonitorInfo& info = |
| config.monitors[base::NumberToString(config.monitors.size())]; |
| info.x = position.x(); |
| info.y = position.y(); |
| info.scale = resolution.dpi().x() == 0.0 |
| ? 1.0 |
| : static_cast<double>(resolution.dpi().x()) / kDefaultDpi; |
| GnomeDisplayConfig::MonitorMode mode; |
| mode.width = resolution.dimensions().width(); |
| mode.height = resolution.dimensions().height(); |
| mode.is_current = true; |
| info.modes.push_back(mode); |
| } |
| |
| inline ScopedGObject<GSettings> CreateGsettingsRegistry() { |
| auto registry = ui::GSettingsNew("org.gnome.desktop.interface"); |
| CHECK(registry) |
| << "ui::GSettingsNew(\"org.gnome.desktop.interface\") failed."; |
| return registry; |
| } |
| |
| } // namespace |
| |
| GnomeDesktopResizer::GnomeDesktopResizer( |
| base::WeakPtr<CaptureStreamManager> stream_manager, |
| base::WeakPtr<GnomeDisplayConfigDBusClient> display_config_client, |
| base::WeakPtr<GnomeDisplayConfigMonitor> display_config_monitor) |
| : GnomeDesktopResizer( |
| stream_manager, |
| display_config_monitor, |
| CreateGsettingsRegistry(), |
| base::BindRepeating( |
| &GnomeDisplayConfigDBusClient::ApplyMonitorsConfig, |
| display_config_client)) {} |
| |
| GnomeDesktopResizer::GnomeDesktopResizer( |
| base::WeakPtr<CaptureStreamManager> stream_manager, |
| base::WeakPtr<GnomeDisplayConfigMonitor> display_config_monitor, |
| ScopedGObject<GSettings> registry, |
| base::RepeatingCallback<void(const GnomeDisplayConfig&)> |
| apply_monitors_config) |
| : stream_manager_(stream_manager), |
| apply_monitors_config_(apply_monitors_config), |
| registry_(std::move(registry)) { |
| if (display_config_monitor) { |
| monitors_changed_subscription_ = display_config_monitor->AddCallback( |
| base::BindRepeating(&GnomeDesktopResizer::OnGnomeDisplayConfigReceived, |
| weak_ptr_factory_.GetWeakPtr()), |
| /*call_with_current_config=*/true); |
| } |
| } |
| |
| GnomeDesktopResizer::~GnomeDesktopResizer() = default; |
| |
| ScreenResolution GnomeDesktopResizer::GetCurrentResolution( |
| webrtc::ScreenId screen_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!stream_manager_) { |
| return {}; |
| } |
| base::WeakPtr<CaptureStream> stream = stream_manager_->GetStream(screen_id); |
| if (!stream) { |
| LOG(ERROR) << "Cannot find pipewire stream for screen ID: " << screen_id; |
| return {}; |
| } |
| |
| double text_scaling_factor = GetTextScalingFactor(); |
| double dpi = kDefaultDpi * text_scaling_factor; |
| auto monitor_it = current_display_config_.FindMonitor(screen_id); |
| if (monitor_it == current_display_config_.monitors.end()) { |
| LOG(ERROR) << "Cannot find monitor with screen ID: " << screen_id; |
| } else { |
| dpi *= monitor_it->second.scale; |
| } |
| return {stream->resolution(), {static_cast<int>(dpi), static_cast<int>(dpi)}}; |
| } |
| |
| std::list<ScreenResolution> GnomeDesktopResizer::GetSupportedResolutions( |
| const ScreenResolution& preferred, |
| webrtc::ScreenId screen_id) { |
| // TODO: crbug.com/431816005 - clamp scale to the supported range of |
| // text-scaling-factor. Also, the effective scale of non-primary displays are |
| // dictated by the preferred scale of the primary display, which may need to |
| // be reflected here. |
| return {preferred}; |
| } |
| |
| void GnomeDesktopResizer::SetResolution(const ScreenResolution& resolution, |
| webrtc::ScreenId screen_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Sometimes the client will send multiple SetResolution requests. The display |
| // layout will become horizontal start-aligned after |
| // SetResolutionAndPosition(), so we only set the preferred layout if it |
| // hasn't been set. |
| if (!preferred_layout_) { |
| preferred_layout_ = current_display_config_.GetLayoutInfo(); |
| MaybeDelayClearPreferredConfig(); |
| } |
| // Note: When changing a monitor's resolution via PipeWire, the monitor order |
| // will also be reset. We could try to preserve the monitor order, but that |
| // will cause existing apps to act strangely. See crbug.com/441824091. |
| SetResolutionAndPosition(resolution, /*position=*/std::nullopt, screen_id); |
| } |
| |
| void GnomeDesktopResizer::RestoreResolution(const ScreenResolution& original, |
| webrtc::ScreenId screen_id) { |
| SetResolution(original, screen_id); |
| } |
| |
| void GnomeDesktopResizer::SetVideoLayout(const protocol::VideoLayout& layout) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!stream_manager_) { |
| return; |
| } |
| auto active_screen_ids = base::MakeFlatSet<webrtc::ScreenId>( |
| stream_manager_->GetActiveStreams(), std::less<>(), |
| [](const auto& kv) { return kv.first; }); |
| base::flat_set<webrtc::ScreenId> screen_ids_in_video_track; |
| for (const auto& track : layout.video_track()) { |
| if (track.has_screen_id()) { |
| screen_ids_in_video_track.emplace(track.screen_id()); |
| } |
| } |
| auto streams_to_be_removed = |
| base::STLSetDifference<base::flat_set<webrtc::ScreenId>>( |
| active_screen_ids, screen_ids_in_video_track); |
| if (!streams_to_be_removed.empty()) { |
| if (!streams_being_removed_.empty()) { |
| LOG(WARNING) << "Streams will not be removed since there are already " |
| << "streams being removed."; |
| } else { |
| // Set `streams_being_removed_` beforehand so that the |
| // SetResolutionAndPosition() calls below know whether they should be |
| // queued up or executed right away. |
| streams_being_removed_ = streams_to_be_removed; |
| for (webrtc::ScreenId stream_id : streams_being_removed_) { |
| stream_manager_->RemoveVirtualStream(stream_id); |
| preferred_monitors_config_.erase(stream_id); |
| } |
| } |
| } |
| |
| // `display_config_for_layout_calculation` is just for calculating the |
| // layout direction and alignment. It is not meant to be passed to |
| // ApplyMonitorsConfig. |
| GnomeDisplayConfig display_config_for_layout_calculation; |
| switch (layout.pixel_type()) { |
| case protocol::VideoLayout::PixelType::VideoLayout_PixelType_LOGICAL: |
| display_config_for_layout_calculation.layout_mode = |
| GnomeDisplayConfig::LayoutMode::kLogical; |
| break; |
| // While we only use logical layout now, physical layout may be passed in |
| // if we are restoring the display layout from a M141 host. We will switch |
| // to logical layout when applying the config. |
| case protocol::VideoLayout::PixelType::VideoLayout_PixelType_PHYSICAL: |
| case protocol::VideoLayout::PixelType:: |
| VideoLayout_PixelType_UNSPECIFIED_PIXEL_TYPE: |
| display_config_for_layout_calculation.layout_mode = |
| GnomeDisplayConfig::LayoutMode::kPhysical; |
| break; |
| } |
| for (const auto& track : layout.video_track()) { |
| // The client doesn't seem to set the initial DPI, so we set it to 1 if |
| // the calculated scale is 0. This allows the correct scale to be used if |
| // the client later decides to send the initial DPI. |
| double scale = static_cast<double>(track.x_dpi()) / kDefaultDpi; |
| if (scale == 0.0) { |
| scale = 1.0; |
| } |
| double physical_resolution_multiplier = |
| layout.pixel_type() == |
| protocol::VideoLayout::PixelType::VideoLayout_PixelType_LOGICAL |
| ? scale |
| : 1.0; |
| |
| webrtc::DesktopSize physical_resolution{ |
| static_cast<int>(track.width() * physical_resolution_multiplier), |
| static_cast<int>(track.height() * physical_resolution_multiplier)}; |
| ScreenResolution screen_resolution{physical_resolution, |
| {track.x_dpi(), track.y_dpi()}}; |
| // If a physical layout is passed, the position will be invalid but the |
| // Relayout() call in DoApplyPreferredMonitorsConfig() will fix it. |
| webrtc::DesktopVector position{track.position_x(), track.position_y()}; |
| |
| if (!track.has_screen_id()) { |
| stream_manager_->AddVirtualStream( |
| screen_resolution, |
| base::BindOnce(&GnomeDesktopResizer::OnAddStreamResult, |
| weak_ptr_factory_.GetWeakPtr(), |
| PreferredMonitorConfig{ |
| .expected_resolution = physical_resolution, |
| .position = position, |
| .scale = scale, |
| })); |
| } else { |
| SetResolutionAndPosition(screen_resolution, position, track.screen_id()); |
| } |
| AddMonitorForLayoutCalculation(display_config_for_layout_calculation, |
| position, screen_resolution); |
| } |
| preferred_layout_ = display_config_for_layout_calculation.GetLayoutInfo(); |
| MaybeDelayClearPreferredConfig(); |
| } |
| |
| base::WeakPtr<GnomeDesktopResizer> GnomeDesktopResizer::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| void GnomeDesktopResizer::SetResolutionAndPosition( |
| const ScreenResolution& resolution, |
| std::optional<webrtc::DesktopVector> position, |
| webrtc::ScreenId screen_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // It is unsafe to call `stream->SetResolution()` when there are streams being |
| // deleted. See comments on `do_after_stream_removal_`. |
| if (!streams_being_removed_.empty()) { |
| do_after_stream_removal_.AddUnsafe(base::BindOnce( |
| &GnomeDesktopResizer::SetResolutionAndPosition, |
| weak_ptr_factory_.GetWeakPtr(), resolution, position, screen_id)); |
| return; |
| } |
| |
| if (!stream_manager_) { |
| return; |
| } |
| // Change the screen resolution. |
| base::WeakPtr<CaptureStream> stream = stream_manager_->GetStream(screen_id); |
| if (!stream) { |
| LOG(ERROR) << "Cannot find pipewire stream for screen ID: " << screen_id; |
| return; |
| } |
| bool resolution_changed = |
| !resolution.dimensions().equals(stream->resolution()); |
| if (resolution_changed) { |
| stream->SetResolution(resolution.dimensions()); |
| } |
| |
| DCHECK_EQ(resolution.dpi().x(), resolution.dpi().y()); |
| double preferred_scale = |
| static_cast<double>(resolution.dpi().x()) / kDefaultDpi; |
| bool has_preferred_config = preferred_monitors_config_.find(screen_id) != |
| preferred_monitors_config_.end(); |
| PreferredMonitorConfig& preferred_config = |
| preferred_monitors_config_[screen_id]; |
| preferred_config.expected_resolution = resolution.dimensions(), |
| preferred_config.scale = preferred_scale; |
| if (position.has_value()) { |
| preferred_config.position = *position; |
| } else if (!has_preferred_config) { |
| // If this is a new config and no position is specified, then we should keep |
| // the current position reported by the DisplayConfig API. |
| auto monitor_it = current_display_config_.FindMonitor(screen_id); |
| if (monitor_it == current_display_config_.monitors.end()) { |
| LOG(ERROR) << "Cannot find monitor with screen ID: " << screen_id; |
| } else { |
| preferred_config.position = {monitor_it->second.x, monitor_it->second.y}; |
| } |
| } |
| MaybeDelayClearPreferredConfig(); |
| |
| // If the resolution has not changed, then we can immediately apply the |
| // preferred monitors config, otherwise we wait for an updated displays config |
| // to be received with a matching screen resolution to learn the list of |
| // supported scales and prevent race conditions. |
| if (!resolution_changed) { |
| ScheduleApplyPreferredMonitorsConfig(); |
| } |
| } |
| |
| void GnomeDesktopResizer::OnAddStreamResult( |
| const PreferredMonitorConfig& monitor_config, |
| CaptureStreamManager::AddStreamResult result) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!result.has_value()) { |
| LOG(ERROR) << "Failed to add stream: " << result.error(); |
| return; |
| } |
| preferred_monitors_config_[result.value()->screen_id()] = monitor_config; |
| MaybeDelayClearPreferredConfig(); |
| ScheduleApplyPreferredMonitorsConfig(); |
| } |
| |
| void GnomeDesktopResizer::OnGnomeDisplayConfigReceived( |
| const GnomeDisplayConfig& config) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| current_display_config_ = config; |
| |
| bool found_stream_pending_removal = false; |
| for (const auto& screen_id : streams_being_removed_) { |
| if (current_display_config_.FindMonitor(screen_id) != |
| current_display_config_.monitors.end()) { |
| found_stream_pending_removal = true; |
| break; |
| } |
| } |
| |
| // Remove monitors that do not have a current mode, which may exist when they |
| // are being created or destroyed. It is safe to ignore them since a new |
| // display config will be received if they later gain a current mode. |
| current_display_config_.RemoveInvalidMonitors(); |
| // This is no-op if the current layout mode is already the preferred layout |
| // mode, otherwise it will recalculate the monitor offsets under the new mode. |
| current_display_config_.SwitchLayoutMode(kPreferredLayoutMode); |
| |
| if (!streams_being_removed_.empty() && !found_stream_pending_removal) { |
| streams_being_removed_.clear(); |
| do_after_stream_removal_.Notify(); |
| } |
| // This is no-op if all the preferred config is reflected in the current |
| // display config. |
| ScheduleApplyPreferredMonitorsConfig(); |
| } |
| |
| void GnomeDesktopResizer::ScheduleApplyPreferredMonitorsConfig() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (apply_monitors_config_scheduled_) { |
| return; |
| } |
| apply_monitors_config_scheduled_ = true; |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&GnomeDesktopResizer::DoApplyPreferredMonitorsConfig, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void GnomeDesktopResizer::DoApplyPreferredMonitorsConfig() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| apply_monitors_config_scheduled_ = false; |
| |
| if (on_trying_to_apply_preferred_monitors_config_for_testing_) { |
| std::move(on_trying_to_apply_preferred_monitors_config_for_testing_).Run(); |
| } |
| |
| if (preferred_monitors_config_.empty()) { |
| return; |
| } |
| // Check if all resolution changes are reflected in the new config. If so, |
| // apply the display scales. Sometimes the new config may already have the |
| // desired scales and positions, in which case we don't want to apply the |
| // display config, since it would show a confirmation dialog. |
| GnomeDisplayConfig new_config = current_display_config_; |
| // There is a bug in mutter such that fractional scales may be reported as |
| // supported but will fail to be applied when there are multiple virtual |
| // monitors, so we ignore fractional scales in that case. |
| // See: https://gitlab.gnome.org/GNOME/mutter/-/issues/4277 |
| bool ignore_fractional_scales = |
| ignore_fractional_scales_in_multimon_ && new_config.monitors.size() > 1; |
| bool config_changed = false; |
| // Code below will early return if not all expected resolution changes have |
| // been reflected in the display config. |
| for (auto [screen_id, preferred_config] : preferred_monitors_config_) { |
| auto monitor_it = new_config.FindMonitor(screen_id); |
| if (monitor_it == new_config.monitors.end()) { |
| // This may happen for newly added monitors that may not be reflected in |
| // `current_display_config_` yet. |
| return; |
| } |
| |
| GnomeDisplayConfig::MonitorInfo& monitor = monitor_it->second; |
| const GnomeDisplayConfig::MonitorMode* mode = monitor.GetCurrentMode(); |
| if (!mode) { |
| LOG(ERROR) << "Cannot find current mode for monitor with screen ID: " |
| << screen_id; |
| return; |
| } else if (!preferred_config.expected_resolution.equals( |
| webrtc::DesktopSize{mode->width, mode->height})) { |
| // Resolution change not reflected in display config yet. |
| return; |
| } |
| if (monitor.x != preferred_config.position.x() || |
| monitor.y != preferred_config.position.y()) { |
| monitor.x = preferred_config.position.x(); |
| monitor.y = preferred_config.position.y(); |
| config_changed = true; |
| } |
| double best_monitor_scale = FindBestScale( |
| preferred_config.scale, monitor.GetCurrentMode()->supported_scales, |
| ignore_fractional_scales); |
| if (monitor.scale != best_monitor_scale) { |
| monitor.scale = best_monitor_scale; |
| config_changed = true; |
| } |
| // For the primary monitor, we correct the effective scale by applying |
| // a text scale. We can't do this for all monitors, since the text scale |
| // is globally applied, so we only do this for the primary monitor. |
| // Note: an integer scale is usually supported, so this is usually only |
| // applied when the client requests a fractional scale for a monitor. |
| if (monitor.is_primary && |
| !IsSameScale(monitor.scale * GetTextScalingFactor(), |
| preferred_config.scale)) { |
| if (registry_ && |
| !g_settings_set_double(registry_.get(), "text-scaling-factor", |
| preferred_config.scale / monitor.scale)) { |
| LOG(ERROR) << "Failed to set text-scaling-factor"; |
| } |
| } |
| } |
| |
| if (preferred_layout_.has_value()) { |
| preferred_layout_->layout_mode = kPreferredLayoutMode; |
| new_config.Relayout(*preferred_layout_); |
| for (const auto& [monitor_name, monitor] : new_config.monitors) { |
| if (!config_changed) { |
| // Check if relayout changes the monitor offsets and update |
| // `config_changed`. Relayout never changes monitor sizes so we don't |
| // need to worry about that. |
| auto current_monitor_it = |
| current_display_config_.monitors.find(monitor_name); |
| DCHECK(current_monitor_it != current_display_config_.monitors.end()); |
| if (current_monitor_it->second.x != monitor.x || |
| current_monitor_it->second.y != monitor.y) { |
| config_changed = true; |
| } |
| } |
| |
| // Write the new offsets back to the preferred config, if it exists. |
| auto it = preferred_monitors_config_.find( |
| GnomeDisplayConfig::GetScreenId(monitor_name)); |
| if (it != preferred_monitors_config_.end()) { |
| it->second.position.set(monitor.x, monitor.y); |
| } |
| } |
| } |
| |
| if (config_changed) { |
| // Setting `method` to `kPersistent` would trigger a confirmation dialog |
| // that would revert the change if the user hasn't clicked "Keep Changes" |
| // within 15 seconds. |
| // See: |
| // https://gitlab.gnome.org/GNOME/mutter/-/blob/1c6532ee18fd72ad324f8f53ccc03bfdf31e90e2/src/backends/meta-monitor-manager.c#L3180 |
| // The difference between kTemporary and kPersistent is that the former |
| // will not write the current display config to the disk, such that, e.g. |
| // the display config will get reverted after device reboots. For CRD, all |
| // the virtual displays are ephemeral, and we track the current display |
| // config and make changes whenever necessary, so kTemporary suffices and |
| // there is no benefit using kPersistent. |
| new_config.method = GnomeDisplayConfig::Method::kTemporary; |
| apply_monitors_config_.Run(new_config); |
| // Only start the timer when it's not running, so that the preferred |
| // config will be cleared if the display config keeps changing/never |
| // stabilizes. |
| if (!clear_preferred_config_timer_.IsRunning()) { |
| clear_preferred_config_timer_.Start( |
| FROM_HERE, kClearPreferredConfigDelay, this, |
| &GnomeDesktopResizer::ClearPreferredConfig); |
| } |
| } |
| } |
| |
| void GnomeDesktopResizer::ClearPreferredConfig() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| preferred_monitors_config_.clear(); |
| preferred_layout_.reset(); |
| streams_being_removed_.clear(); |
| do_after_stream_removal_.Clear(); |
| } |
| |
| void GnomeDesktopResizer::MaybeDelayClearPreferredConfig() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (clear_preferred_config_timer_.IsRunning()) { |
| clear_preferred_config_timer_.Reset(); |
| } |
| } |
| |
| double GnomeDesktopResizer::GetTextScalingFactor() const { |
| if (!registry_) { |
| return 1.0; |
| } |
| return g_settings_get_double(registry_.get(), "text-scaling-factor"); |
| } |
| |
| } // namespace remoting |