8000 Various fixes and improvements for v4.0.1 (#769) · MonitorControl/MonitorControl@cfc77d5 · GitHub
[go: up one dir, main page]

Skip to content

Commit cfc77d5

Browse files
authored
Various fixes and improvements for v4.0.1 (#769)
- Fixed: display properties reset turns off hardware DDC - Fixed: brief black screen upon changing space when using shade dimming - Asynchronous thread-safe debouncing DDC write for smoother sliders - Improved support for [BetterDummy](https://github.com/waydabber/BetterDummy) - Better support for common physical dummies identifying as 28E850 - Inert dummy menu sliders are now hidden - Improved support for nongamma->nongamma mirroring scenarios - Compiled to run 10.14 (needed some minor changes) - compatibility is unofficial! - Updated README - Bumped version number to 4.0.1 (service release)
1 parent 9f93840 commit cfc77d5

File tree

11 files changed

+142
-89
lines changed

11 files changed

+142
-89
lines changed

.swiftlint.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ disabled_rules:
44
- identifier_name
55
- trailing_comma
66
type_body_length: 500
7-
file_length: 500
7+
file_length: 750
88
cyclomatic_complexity:
99
ignores_case_statements: true
1010
opening_brace:

MonitorControl.xcodeproj/project.pbxproj

+6-6
Original file line numberDiff line numberDiff line change
@@ -751,8 +751,8 @@
751751
"$(inherited)",
752752
"@executable_path/../Frameworks",
753753
);
754-
MACOSX_DEPLOYMENT_TARGET = 10.15;
755-
MARKETING_VERSION = 4.0.0;
754+
MACOSX_DEPLOYMENT_TARGET = 10.14;
755+
MARKETING_VERSION = 4.0.1;
756756
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl;
757757
PRODUCT_NAME = "$(TARGET_NAME)";
758758
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -786,8 +786,8 @@
786786
"$(inherited)",
787787
"@executable_path/../Frameworks",
788788
);
789-
MACOSX_DEPLOYMENT_TARGET = 10.15;
790-
MARKETING_VERSION = 4.0.0;
789+
MACOSX_DEPLOYMENT_TARGET = 10.14;
790+
MARKETING_VERSION = 4.0.1;
791791
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControl;
792792
PRODUCT_NAME = "$(TARGET_NAME)";
793793
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -821,7 +821,7 @@
821821
"$(inherited)",
822822
"@executable_path/../Frameworks",
823823
);
824-
MACOSX_DEPLOYMENT_TARGET = 10.15;
824+
MACOSX_DEPLOYMENT_TARGET = 10.14;
825825
MARKETING_VERSION = 4.0.1;
826826
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControlHelper;
827827
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -850,7 +850,7 @@
850850
"$(inherited)",
851851
"@executable_path/../Frameworks",
852852
);
853-
MACOSX_DEPLOYMENT_TARGET = 10.15;
853+
MACOSX_DEPLOYMENT_TARGET = 10.14;
854854
MARKETING_VERSION = 4.0.1;
855855
PRODUCT_BUNDLE_IDENTIFIER = me.guillaumeb.MonitorControlHelper;
856856
PRODUCT_NAME = "$(TARGET_NAME)";

MonitorControl/Info.plist

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<key>CFBundleShortVersionString</key>
2020
<string>$(MARKETING_VERSION)</string>
2121
<key>CFBundleVersion</key>
22-
<string>6851</string>
22+
<string>6941</string>
2323
<key>LSApplicationCategoryType</key>
2424
<string>public.app-category.utilities</string>
2525
<key>LSMinimumSystemVersion</key>

MonitorControl/Model/Display.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ class Display: Equatable {
230230
return
231231
}
232232
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
233-
_ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: self.identifier)
233+
_ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
234234
} else {
235235
let gammaTableRed = self.defaultGammaTableRed.map { $0 * transientValue }
236236
let gammaTableGreen = self.defaultGammaTableGreen.map { $0 * transientValue }
@@ -243,7 +243,7 @@ class Display: Equatable {
243243
} else {
244244
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
245245
self.swBrightnessSemaphore.signal()
246-
return DisplayManager.shared.setShadeAlpha(value: 1 - newValue, displayID: self.identifier)
246+
return DisplayManager.shared.setShadeAlpha(value: 1 - newValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
247247
} else {
248248
let gammaTableRed = self.defaultGammaTableRed.map { $0 * newValue }
249249
let gammaTableGreen = self.defaultGammaTableGreen.map { $0 * newValue }
@@ -267,7 +267,7 @@ class Display: Equatable {
267267
}
268268
self.swBrightnessSemaphore.wait()
269269
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
270-
let rawBrightnessValue = 1 - (DisplayManager.shared.getShadeAlpha(displayID: self.identifier) ?? 1)
270+
let rawBrightnessValue = 1 - (DisplayManager.shared.getShadeAlpha(displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier)) ?? 1)
271271
self.swBrightnessSemaphore.signal()
272272
return self.swBrightnessTransform(value: rawBrightnessValue, reverse: true)
273273
}

MonitorControl/Model/OtherDisplay.swift

+43-27
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class OtherDisplay: Display {
99
var arm64ddc: Bool = false
1010
var arm64avService: IOAVService?
1111
var isDiscouraged: Bool = false
12+
let writeDDCQueue = DispatchQueue(label: "Local write DDC queue")
13+
var writeDDCNextValue: [Command: UInt16] = [:]
14+
var writeDDCLastSavedValue: [Command: UInt16] = [:]
1215
var pollingCount: Int {
1316
get {
1417
switch self.readPrefAsInt(key: .pollingMode) {
@@ -76,11 +79,11 @@ class OtherDisplay: Display {
7679
if !self.smoothBrightnessRunning, !self.isSw(), !self.readPrefAsBool(key: .unavailableDDC, for: command), self.readPrefAsBool(key: .isTouched, for: command), prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.write.rawValue, !app.safeMode {
7780
let restoreValue = self.getDDCValueFromPrefs(command)
7881
os_log("Restoring %{public}@ DDC value %{public}@ for %{public}@", type: .info, String(reflecting: command), String(restoreValue), self.name)
79-
_ = self.writeDDCValues(command: command, value: restoreValue)
82+
self.writeDDCValues(command: command, value: restoreValue)
8083
if command == .audioSpeakerVolume, self.readPrefAsBool(key: .enableMuteUnmute) {
8184
let currentMuteValue = self.readPrefAsInt(for: .audioMuteScreenBlank) == 0 ? 2 : self.readPrefAsInt(for: .audioMuteScreenBlank)
8285
os_log("- Writing last saved DDC value for Mute: %{public}@", type: .info, String(currentMuteValue))
83-
_ = self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(currentMuteValue))
86+
self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(currentMuteValue))
8487
}
8588
}
8689
}
@@ -178,13 +181,11 @@ class OtherDisplay: Display {
178181
let isAlreadySet = volumeOSDValue == self.readPrefAsFloat(for: .audioSpeakerVolume)
179182
if !isAlreadySet {
180183
if let muteValue = muteValue, self.readPrefAsBool(key: .enableMuteUnmute) {
181-
guard self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else {
182-
return
183-
}
184+
self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue))
184185
self.savePref(muteValue, for: .audioMuteScreenBlank)
185186
}
186187
if !self.readPrefAsBool(key: .enableMuteUnmute) || volumeOSDValue != 0 {
187-
_ = self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
188+
self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
188189
}
189190
}
190191
if !self.readPrefAsBool(key: .hideOsd) {
@@ -206,7 +207,7 @@ class OtherDisplay: Display {
206207
let contrastOSDValue = self.calcNewValue(currentValue: currentValue, isUp: isUp, isSmallIncrement: isSmallIncrement)
207208
let isAlreadySet = contrastOSDValue == self.readPrefAsFloat(for: .contrast)
208209
if !isAlreadySet {
209-
_ = self.writeDDCValues(command: .contrast, value: self.convValueToDDC(for: .contrast, from: contrastOSDValue))
210+
self.writeDDCValues(command: .contrast, value: self.convValueToDDC(for: .contrast, from: contrastOSDValue))
210211
}
211212
OSDUtils.showOsd(displayID: self.identifier, command: .contrast, value: contrastOSDValue, roundChiclet: !isSmallIncrement)
212213
if !isAlreadySet {
@@ -237,13 +238,11 @@ class OtherDisplay: Display {
237238
}
238239
}
239240
if self.readPrefAsBool(key: .enableMuteUnmute) {
240-
guard self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue)) == true else {
241-
return
242-
}
241+
self.writeDDCValues(command: .audioMuteScreenBlank, value: UInt16(muteValue))
243242
}
244243
self.savePref(muteValue, for: .audioMuteScreenBlank)
245244
if !self.readPrefAsBool(key: .enableMuteUnmute) || volumeOSDValue > 0 {
246-
_ = self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
245+
self.writeDDCValues(command: .audioSpeakerVolume, value: self.convValueToDDC(for: .audioSpeakerVolume, from: volumeOSDValue))
247246
}
248247
if !fromVolumeSlider {
249248
if !self.readPrefAsBool(key: .hideOsd) {
@@ -345,12 +344,12 @@ class OtherDisplay: Display {
345344
brightnessValue = 0
346345
brightnessSwValue = (value / self.combinedBrightnessSwitchingValue())
347346
}
348-
_ = self.writeDDCValues(command: .brightness, value: self.convValueToDDC(for: .brightness, from: brightnessValue))
347+
self.writeDDCValues(command: .brightness, value: self.convValueToDDC(for: .brightness, from: brightnessValue))
349348
if self.readPrefAsFloat(key: .SwBrightness) != brightnessSwValue {
350349
_ = self.setSwBrightness(brightnessSwValue)
351350
}
352351
} else {
353-
_ = self.writeDDCValues(command: .brightness, value: self.convValueToDDC(for: .brightness, from: value))
352+
self.writeDDCValues(command: .brightness, value: self.convValueToDDC(for: .brightness, from: value))
354353
}
355354
if !transient {
356355
self.savePref(value, for: .brightness)
@@ -378,28 +377,45 @@ class OtherDisplay: Display {
378377
return intCodes
379378
}
380379

381-
public func writeDDCValues(command: Command, value: UInt16, errorRecoveryWaitTime _: UInt32? = nil) -> Bool? {
380+
public func writeDDCValues(command: Command, value: UInt16) {
382381
guard app.sleepID == 0, app.reconfigureID == 0, !self.readPrefAsBool(key: .forceSw), !self.readPrefAsBool(key: .unavailableDDC, for: command) else {
383-
return false
382+
return
383+
}
384+
self.writeDDCQueue.async(flags: .barrier) {
385+
self.writeDDCNextValue[command] = value
386+
}
387+
DisplayManager.shared.globalDDCQueue.async(flags: .barrier) {
388+
self.asyncPerformWriteDDCValues(command: command)
389+
}
390+
}
391+
392+
func asyncPerformWriteDDCValues(command: Command) {
393+
var value = UInt16.max
394+
var lastValue = UInt16.max
395+
self.writeDDCQueue.sync {
396+
value = self.writeDDCNextValue[command] ?? UInt16.max
397+
lastValue = self.writeDDCLastSavedValue[command] ?? UInt16.max
398+
}
399+
guard value != UInt16.max, value != lastValue else {
400+
return
401+
}
402+
self.writeDDCQueue.async(flags: .barrier) {
403+
self.writeDDCLastSavedValue[command] = value
404+
self.savePref(true, key: PrefKey.isTouched, for: command)
384405
}
385-
var success: Bool = false
386406
var controlCodes = self.getRemapControlCodes(command: command)
387407
if controlCodes.count == 0 {
388408
controlCodes.append(command.rawValue)
389409
}
390410
for controlCode in controlCodes {
391-
DisplayManager.shared.ddcQueue.sync {
392-
if Arm64DDC.isArm64 {
393-
if self.arm64ddc {
394-
success = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value)
395-
}
396-
} else {
397-
success = self.ddc?.write(command: command.rawValue, value: value, errorRecoveryWaitTime: 2000) ?? false
411+
if Arm64DDC.isArm64 {
412+
if self.arm64ddc {
413+
_ = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value)
398 10000 414
}
399-
self.savePref(true, key: PrefKey.isTouched, for: command) // We deliberatly consider the value tuched no matter if the call succeeded
415+
} else {
416+
_ = self.ddc?.write(command: controlCode, value: value, errorRecoveryWaitTime: 2000) ?? false
400417
}
401418
}
402-
return success
403419
}
404420

405421
func readDDCValues(for command: Command, tries: UInt, minReplyDelay delay: UInt64?) -> (current: UInt16, max: UInt16)? {
@@ -413,15 +429,15 @@ class OtherDisplay: Display {
413429
guard self.arm64ddc else {
414430
return nil
415431
}
416-
DisplayManager.shared.ddcQueue.sync {
432+
DisplayManager.shared.globalDDCQueue.sync {
417433
if let unwrappedDelay = delay {
418434
values = Arm64DDC.read(service: self.arm64avService, command: controlCode, tries: UInt8(min(tries, 255)), minReplyDelay: UInt32(unwrappedDelay / 1000))
419435
} else {
420436
values = Arm64DDC.read(service: self.arm64avService, command: controlCode, tries: UInt8(min(tries, 255)))
421437
}
422438
}
423439
} else {
424-
DisplayManager.shared.ddcQueue.sync {
440+
DisplayManager.shared.globalDDCQueue.sync {
425441
values = self.ddc?.read(command: controlCode, tries: tries, minReplyDelay: delay)
426442
}
427443
}

MonitorControl/Support/DisplayManager.swift

+56-26
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class DisplayManager {
99

1010
var displays: [Display] = []
1111
var audioControlTargetDisplays: [OtherDisplay] = []
12-
let ddcQueue = DispatchQueue(label: "DDC queue")
12+
let globalDDCQueue = DispatchQueue(label:< 10000 /span> "Global DDC queue")
1313
let gammaActivityEnforcer = NSWindow(contentRect: .init(origin: NSPoint(x: 0, y: 0), size: .init(width: DEBUG_GAMMA_ENFORCER ? 15 : 1, height: DEBUG_GAMMA_ENFORCER ? 15 : 1)), styleMask: [], backing: .buffered, defer: false)
1414
var gammaInterferenceCounter = 0
1515
var gammaInterferenceWarningShown = false
@@ -43,22 +43,39 @@ class DisplayManager {
4343
internal var shades: [CGDirectDisplayID: NSWindow] = [:]
4444
internal var shadeGrave: [NSWindow] = []
4545

46-
func isDisqualifiedFromShade(_ displayID: CGDirectDisplayID) -> Bool { // We ban mirror members from shade control as it might lead to double control
47-
return (CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0) ? true : false
46+
func isDisqualifiedFromShade(_ displayID: CGDirectDisplayID) -> Bool {
47+
if CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0 {
48+
if displayID == DisplayManager.resolveEffectiveDisplayID(displayID), DisplayManager.isVirtual(displayID: displayID) || DisplayManager.isDummy(displayID: displayID) {
49+
var displayIDs = [CGDirectDisplayID](repeating: 0, count: 16)
50+
var displayCount: UInt32 = 0
51+
guard CGGetOnlineDisplayList(16, &displayIDs, &displayCount) == .success else {
52+
return true
53+
}
54+
for displayId in displayIDs where CGDisplayMirrorsDisplay(displayId) == displayID && !DisplayManager.isVirtual(displayID: displayID) {
55+
return true
56+
}
57+
return false
58+
}
59+
return true
60+
}
61+
return false
4862
}
4963

5064
internal func createShadeOnDisplay(displayID: CGDirectDisplayID) -> NSWindow? {
5165
if let screen = DisplayManager.getByDisplayID(displayID: displayID) {
5266
let shade = NSWindow(contentRect: .init(origin: NSPoint(x: 0, y: 0), size: .init(width: 10, height: 1)), styleMask: [], backing: .buffered, defer: false)
5367
shade.title = "Monitor Control Window Shade for Display " + String(displayID)
5468
shade.isMovableByWindowBackground = false
55-
shade.backgroundColor = .black
69+
shade.backgroundColor = .clear
5670
shade.ignoresMouseEvents = true
5771
shade.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
58-
shade.alphaValue = 0
5972
shade.orderFrontRegardless()
6073
shade.collectionBehavior = [.stationary, .canJoinAllSpaces, .ignoresCycle]
6174
shade.setFrame(screen.frame, display: true)
75+
shade.contentView?.wantsLayer = true
76+
shade.contentView?.alphaValue = 0.0
77+
shade.contentView?.layer?.backgroundColor = .black
78+
shade.contentView?.setNeedsDisplay(shade.frame)
6279
os_log("Window shade created for display %{public}@", type: .info, String(displayID))
6380
return shade
6481
}
@@ -125,7 +142,7 @@ class DisplayManager {
125142
return 1
126143
}
127144
if let shade = getShade(displayID: displayID) {
128-
return Float(shade.alphaValue)
145+
return Float(shade.contentView?.alphaValue ?? 1)
129146
} else {
130147
return 1
131148
}
@@ -136,7 +153,7 @@ class DisplayManager {
136153
return false
137154
}
138155
if let shade = getShade(displayID: displayID) {
139-
shade.alphaValue = CGFloat(value)
156+
shade.contentView?.alphaValue = CGFloat(value)
140157
return true
141158
}
142159
return false
@@ -151,27 +168,12 @@ class DisplayManager {
151168
return
152169
}
153170
for onlineDisplayID in onlineDisplayIDs where onlineDisplayID != 0 {
154-
let rawName = DisplayManager.getDisplayRawNameByID(displayID: onlineDisplayID)
155171
let name = DisplayManager.getDisplayNameByID(displayID: onlineDisplayID)
156172
let id = onlineDisplayID
157173
let vendorNumber = CGDisplayVendorNumber(onlineDisplayID)
158174
let modelNumber = CGDisplayModelNumber(onlineDisplayID)
159-
var isDummy: Bool = false
160-
var isVirtual: Bool = false
161-
if rawName == "28E850" || rawName.lowercased().contains("dummy") {
162-
os_log("NOTE: Display is a dummy!", type: .info)
163-
isDummy = true
164-
}
165-
if !DEBUG_MACOS10, #available(macOS 11.0, *) {
166-
if let dictionary = ((CoreDisplay_DisplayCreateInfoDictionary(onlineDisplayID))?.takeRetainedValue() as NSDictionary?) {
167-
let isVirtualDevice = dictionary["kCGDisplayIsVirtualDevice"] as? Bool
168-
let displayIsAirplay = dictionary["kCGDisplayIsAirPlay"] as? Bool
169-
if isVirtualDevice ?? displayIsAirplay ?? false {
170-
os_log("NOTE: Display is virtual!", type: .info)
171-
isVirtual = true
172-
}
173-
}
174-
}
175+
let isDummy: Bool = DisplayManager.isDummy(displayID: onlineDisplayID)
176+
let isVirtual: Bool = DisplayManager.isVirtual(displayID: onlineDisplayID)
175177
if !DEBUG_SW, DisplayManager.isAppleDisplay(displayID: onlineDisplayID) { // MARK: (point of interest for testing)
176178
let appleDisplay = AppleDisplay(id, name: name, vendorNumber: vendorNumber, modelNumber: modelNumber, isVirtual: isVirtual, isDummy: isDummy)
177179
os_log("Apple display found - %{public}@", type: .info, "ID: \(appleDisplay.identifier), Name: \(appleDisplay.name) (Vendor: \(appleDisplay.vendorNumber ?? 0), Model: \(appleDisplay.modelNumber ?? 0))")
@@ -391,6 +393,30 @@ class DisplayManager {
391393
return affectedDisplays
392394
}
393395

396+
static func isDummy(displayID: CGDirectDisplayID) -> Bool {
397+
let rawName = DisplayManager.getDisplayRawNameByID(displayID: displayID)
398+
var isDummy: Bool = false
399+
if rawName == "28E850" || rawName.lowercased().contains("dummy") {
400+
os_log("NOTE: Display is a dummy!", type: .info)
401+
isDummy = true
402+
}
403+
return isDummy
404+
}
405+
406+
static func isVirtual(displayID: CGDirectDisplayID) -> Bool {
407+
var isVirtual: Bool = false
408+
if !DEBUG_MACOS10, #available(macOS 11.0, *) {
409+
if let dictionary = ((CoreDisplay_DisplayCreateInfoDictionary(displayID))?.takeRetainedValue() as NSDictionary?) {
410+
let isVirtualDevice = dictionary["kCGDisplayIsVirtualDevice"] as? Bool
411+
let displayIsAirplay = dictionary["kCGDisplayIsAirPlay"] as? Bool
412+
if isVirtualDevice ?? displayIsAirplay ?? false {
413+
isVirtual = true
414+
}
415+
}
416+
}
417+
return isVirtual
418+
}
419+
394420
static func engageMirror() -> Bool {
395421
var onlineDisplayIDs = [CGDirectDisplayID](repeating: 0, count: 16)
396422
var displayCount: UInt32 = 0
@@ -478,14 +504,18 @@ class DisplayManager {
478504
if CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0 {
479505
let mirroredDisplayID = CGDisplayMirrorsDisplay(displayID)
480506
if mirroredDisplayID != 0, let dictionary = ((CoreDisplay_DisplayCreateInfoDictionary(mirroredDisplayID))?.takeRetainedValue() as NSDictionary?), let nameList = dictionary["DisplayProductName"] as? [String: String], let mirroredName = nameList[Locale.current.identifier] ?? nameList["en_US"] ?? nameList.first?.value {
481-
name.append("~" + mirroredName)
507+
name.append(" | " + mirroredName)
482508
}
483509
}
484510
return name
485511
}
486512
}
487513
if let screen = getByDisplayID(displayID: displayID) { // MARK: This, and NSScreen+Extension.swift will not be needed when we drop MacOS 10 support.
488-
return screen.localizedName
514+
if #available(macOS 10.15, *) {
515+
return screen.localizedName
516+
} else {
517+
return screen.displayName ?? defaultName
518+
}
489519
}
490520
return defaultName
491521
}

0 commit comments

Comments
 (0)
0