diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..843f5d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + + +## User settings +xcuserdata/ + +## Xcode 8 and earlier +*.xcscmblueprint +*.xccheckout + + + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bca3343 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Hiroyasu Niitsuma + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bf1738 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# ScrollableSimulator +An app that enables scrolling functionality in Simulator.app. + +# Feature +- You can scroll using a trackpad or mouse on Simulator.app with this application. +- No more dragging with the trackpad on Simulator.app! + +# Download +## GitHub Assets +TODO + +## AppStore +Coming soon... + +# Demo diff --git a/ScrollableSimulator.xcodeproj/project.pbxproj b/ScrollableSimulator.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0ac0b90 --- /dev/null +++ b/ScrollableSimulator.xcodeproj/project.pbxproj @@ -0,0 +1,404 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 4F7744F62B65ED4E00741931 /* ScrollableSimulatorBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7744F52B65ED4E00741931 /* ScrollableSimulatorBehavior.swift */; }; + 4F7744FA2B662D8000741931 /* MouseScrollCompletionCaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7744F92B662D8000741931 /* MouseScrollCompletionCaller.swift */; }; + 4F7744FC2B6631AF00741931 /* TrackpadScrollBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7744FB2B6631AF00741931 /* TrackpadScrollBehavior.swift */; }; + 4F7744FE2B6631DF00741931 /* CGEvent+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7744FD2B6631DF00741931 /* CGEvent+.swift */; }; + 4F7745002B66336300741931 /* MouseScrollBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7744FF2B66336300741931 /* MouseScrollBehavior.swift */; }; + 4F8097DB2B65288E00192C7F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8097DA2B65288E00192C7F /* AppDelegate.swift */; }; + 4F8097DF2B65289000192C7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F8097DE2B65289000192C7F /* Assets.xcassets */; }; + 4F8097EA2B6528A000192C7F /* ScrollableSimulatorLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8097E92B6528A000192C7F /* ScrollableSimulatorLauncher.swift */; }; + 4FEF63322B66452E00BA2032 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4FEF63312B66452E00BA2032 /* Main.storyboard */; }; + 4FEF63342B6646EB00BA2032 /* SimulatorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF63332B6646EB00BA2032 /* SimulatorApp.swift */; }; + 4FEF63362B6647B300BA2032 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF63352B6647B300BA2032 /* Accessibility.swift */; }; + 4FEF63382B6649C900BA2032 /* AppInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF63372B6649C900BA2032 /* AppInformation.swift */; }; + 4FEF63402B67F72F00BA2032 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF633F2B67F72F00BA2032 /* Logger.swift */; }; + 4FEF63422B67F98C00BA2032 /* AppService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF63412B67F98C00BA2032 /* AppService.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 4F7744F52B65ED4E00741931 /* ScrollableSimulatorBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableSimulatorBehavior.swift; sourceTree = ""; }; + 4F7744F92B662D8000741931 /* MouseScrollCompletionCaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseScrollCompletionCaller.swift; sourceTree = ""; }; + 4F7744FB2B6631AF00741931 /* TrackpadScrollBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollBehavior.swift; sourceTree = ""; }; + 4F7744FD2B6631DF00741931 /* CGEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGEvent+.swift"; sourceTree = ""; }; + 4F7744FF2B66336300741931 /* MouseScrollBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseScrollBehavior.swift; sourceTree = ""; }; + 4F8097D72B65288E00192C7F /* Scrollable Simulator for debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Scrollable Simulator for debug.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F8097DA2B65288E00192C7F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 4F8097DE2B65289000192C7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4F8097E32B65289000192C7F /* ScrollableSimulator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ScrollableSimulator.entitlements; sourceTree = ""; }; + 4F8097E92B6528A000192C7F /* ScrollableSimulatorLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableSimulatorLauncher.swift; sourceTree = ""; }; + 4FEF63312B66452E00BA2032 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; + 4FEF63332B6646EB00BA2032 /* SimulatorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorApp.swift; sourceTree = ""; }; + 4FEF63352B6647B300BA2032 /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; + 4FEF63372B6649C900BA2032 /* AppInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInformation.swift; sourceTree = ""; }; + 4FEF633B2B664FD400BA2032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 4FEF633F2B67F72F00BA2032 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 4FEF63412B67F98C00BA2032 /* AppService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppService.swift; sourceTree = ""; }; + 4FEF63432B67FB7C00BA2032 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4F8097D42B65288E00192C7F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4F8097CE2B65288E00192C7F = { + isa = PBXGroup; + children = ( + 4FEF63432B67FB7C00BA2032 /* README.md */, + 4F8097D92B65288E00192C7F /* ScrollableSimulator */, + 4F8097D82B65288E00192C7F /* Products */, + ); + sourceTree = ""; + }; + 4F8097D82B65288E00192C7F /* Products */ = { + isa = PBXGroup; + children = ( + 4F8097D72B65288E00192C7F /* Scrollable Simulator for debug.app */, + ); + name = Products; + sourceTree = ""; + }; + 4F8097D92B65288E00192C7F /* ScrollableSimulator */ = { + isa = PBXGroup; + children = ( + 4F8097DA2B65288E00192C7F /* AppDelegate.swift */, + 4FEF63372B6649C900BA2032 /* AppInformation.swift */, + 4FEF63412B67F98C00BA2032 /* AppService.swift */, + 4FEF63332B6646EB00BA2032 /* SimulatorApp.swift */, + 4FEF63352B6647B300BA2032 /* Accessibility.swift */, + 4FEF632C2B66418100BA2032 /* Util */, + 4FEF632B2B66415F00BA2032 /* ScrollableSimulator */, + 4F8097DE2B65289000192C7F /* Assets.xcassets */, + 4F8097E32B65289000192C7F /* ScrollableSimulator.entitlements */, + 4FEF63312B66452E00BA2032 /* Main.storyboard */, + 4FEF633B2B664FD400BA2032 /* Info.plist */, + ); + path = ScrollableSimulator; + sourceTree = ""; + }; + 4FEF632B2B66415F00BA2032 /* ScrollableSimulator */ = { + isa = PBXGroup; + children = ( + 4F8097E92B6528A000192C7F /* ScrollableSimulatorLauncher.swift */, + 4F7744F52B65ED4E00741931 /* ScrollableSimulatorBehavior.swift */, + 4F7744FB2B6631AF00741931 /* TrackpadScrollBehavior.swift */, + 4F7744FF2B66336300741931 /* MouseScrollBehavior.swift */, + 4F7744F92B662D8000741931 /* MouseScrollCompletionCaller.swift */, + ); + path = ScrollableSimulator; + sourceTree = ""; + }; + 4FEF632C2B66418100BA2032 /* Util */ = { + isa = PBXGroup; + children = ( + 4F7744FD2B6631DF00741931 /* CGEvent+.swift */, + 4FEF633F2B67F72F00BA2032 /* Logger.swift */, + ); + path = Util; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4F8097D62B65288E00192C7F /* ScrollableSimulator */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4F8097E62B65289000192C7F /* Build configuration list for PBXNativeTarget "ScrollableSimulator" */; + buildPhases = ( + 4F8097D32B65288E00192C7F /* Sources */, + 4F8097D42B65288E00192C7F /* Frameworks */, + 4F8097D52B65288E00192C7F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ScrollableSimulator; + productName = ScrollableSimulator; + productReference = 4F8097D72B65288E00192C7F /* Scrollable Simulator for debug.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4F8097CF2B65288E00192C7F /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1520; + LastUpgradeCheck = 1520; + TargetAttributes = { + 4F8097D62B65288E00192C7F = { + CreatedOnToolsVersion = 15.2; + }; + }; + }; + buildConfigurationList = 4F8097D22B65288E00192C7F /* Build configuration list for PBXProject "ScrollableSimulator" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4F8097CE2B65288E00192C7F; + productRefGroup = 4F8097D82B65288E00192C7F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4F8097D62B65288E00192C7F /* ScrollableSimulator */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4F8097D52B65288E00192C7F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F8097DF2B65289000192C7F /* Assets.xcassets in Resources */, + 4FEF63322B66452E00BA2032 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4F8097D32B65288E00192C7F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4FEF63342B6646EB00BA2032 /* SimulatorApp.swift in Sources */, + 4FEF63422B67F98C00BA2032 /* AppService.swift in Sources */, + 4F7744FE2B6631DF00741931 /* CGEvent+.swift in Sources */, + 4F7744FC2B6631AF00741931 /* TrackpadScrollBehavior.swift in Sources */, + 4F7744FA2B662D8000741931 /* MouseScrollCompletionCaller.swift in Sources */, + 4F7745002B66336300741931 /* MouseScrollBehavior.swift in Sources */, + 4F7744F62B65ED4E00741931 /* ScrollableSimulatorBehavior.swift in Sources */, + 4FEF63402B67F72F00BA2032 /* Logger.swift in Sources */, + 4F8097DB2B65288E00192C7F /* AppDelegate.swift in Sources */, + 4FEF63362B6647B300BA2032 /* Accessibility.swift in Sources */, + 4FEF63382B6649C900BA2032 /* AppInformation.swift in Sources */, + 4F8097EA2B6528A000192C7F /* ScrollableSimulatorLauncher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 4F8097E42B65289000192C7F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.6; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 4F8097E52B65289000192C7F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.6; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 4F8097E72B65289000192C7F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = ScrollableSimulator/ScrollableSimulator.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2CMV7D36JC; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ScrollableSimulator/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainStoryboardFile = Main; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 0.1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nhiro1109.debug.ScrollableSimulator; + PRODUCT_NAME = "Scrollable Simulator for debug"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 4F8097E82B65289000192C7F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = ScrollableSimulator/ScrollableSimulator.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2CMV7D36JC; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ScrollableSimulator/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSMainStoryboardFile = Main; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 0.1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nhiro1109.ScrollableSimulator; + PRODUCT_NAME = "Scrollable Simulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4F8097D22B65288E00192C7F /* Build configuration list for PBXProject "ScrollableSimulator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4F8097E42B65289000192C7F /* Debug */, + 4F8097E52B65289000192C7F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4F8097E62B65289000192C7F /* Build configuration list for PBXNativeTarget "ScrollableSimulator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4F8097E72B65289000192C7F /* Debug */, + 4F8097E82B65289000192C7F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4F8097CF2B65288E00192C7F /* Project object */; +} diff --git a/ScrollableSimulator.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ScrollableSimulator.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ScrollableSimulator.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ScrollableSimulator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ScrollableSimulator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ScrollableSimulator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ScrollableSimulator.xcodeproj/xcshareddata/xcschemes/ScrollableSimulator.xcscheme b/ScrollableSimulator.xcodeproj/xcshareddata/xcschemes/ScrollableSimulator.xcscheme new file mode 100644 index 0000000..ee2b0fc --- /dev/null +++ b/ScrollableSimulator.xcodeproj/xcshareddata/xcschemes/ScrollableSimulator.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ScrollableSimulator/Accessibility.swift b/ScrollableSimulator/Accessibility.swift new file mode 100644 index 0000000..9efd425 --- /dev/null +++ b/ScrollableSimulator/Accessibility.swift @@ -0,0 +1,34 @@ +import Cocoa + +func showAccessibilityPermissionsAlert() { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = "Requesting access to accessibility features" + alert.informativeText = """ + You can manage this permission in System Preferences > Security & Privacy > Privacy. + + After setting, please restart the application. + """ + alert.addButton(withTitle: "Open Settings and Quit") + let response = alert.runModal() + + switch response { + case .alertFirstButtonReturn: + openAccessibilityForSystemPreference() + NSWorkspace.shared.selectFile( + APP_URL.path, + inFileViewerRootedAtPath: APP_URL.deletingLastPathComponent().path + ) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + NSApplication.shared.terminate(nil) + } + default: + break + } +} + +func openAccessibilityForSystemPreference() { + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { + NSWorkspace.shared.open(url) + } +} diff --git a/ScrollableSimulator/AppDelegate.swift b/ScrollableSimulator/AppDelegate.swift new file mode 100644 index 0000000..271bb1f --- /dev/null +++ b/ScrollableSimulator/AppDelegate.swift @@ -0,0 +1,18 @@ +import Cocoa + +@main +class AppDelegate: NSObject, NSApplicationDelegate { + private let appService: AppService = .init() + + func applicationDidFinishLaunching(_ aNotification: Notification) { + appService.initialize() + } + + func applicationWillTerminate(_ aNotification: Notification) { + appService.terminate() + } + + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/ScrollableSimulator/AppInformation.swift b/ScrollableSimulator/AppInformation.swift new file mode 100644 index 0000000..062671e --- /dev/null +++ b/ScrollableSimulator/AppInformation.swift @@ -0,0 +1,15 @@ +import Foundation + +var APP_NAME: String { + if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { + return appName + } else if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String { + return appName + } else { + return "" + } +} + +var APP_URL: URL! { + return Bundle.main.bundleURL +} diff --git a/ScrollableSimulator/AppService.swift b/ScrollableSimulator/AppService.swift new file mode 100644 index 0000000..a72a4c9 --- /dev/null +++ b/ScrollableSimulator/AppService.swift @@ -0,0 +1,85 @@ +import Cocoa + +class AppService { + private var scrollableSimulator: ScrollableSimulatorLauncher? + private var didTerminateAppObserver: NSObjectProtocol? + private var didLaunchAppAppObserver: NSObjectProtocol? + + func initialize() { + if AXIsProcessTrusted() { + observeDidLaunchApplication() + observeDidTerminateApplication() + if let pid = SimulatorApp.getSimulatorPID() { + launchScrollableSimulator(pid: pid) + } + } else { + showAccessibilityPermissionsAlert() + } + } + + func terminate() { + scrollableSimulator?.deactivate() + } + + private func launchScrollableSimulator(pid: pid_t) { + scrollableSimulator = .init(simulatorPID: pid) + do { + try scrollableSimulator?.activate() + } catch { + showAlertForFailedLaunching(retryHandler: { + launchScrollableSimulator(pid: pid) + }) + } + } + + private func observeDidLaunchApplication() { + didLaunchAppAppObserver = NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didLaunchApplicationNotification, object: nil, queue: .main, using: { [weak self] notification in + guard let self else { return } + if isSimulator(for: notification), let pid = getSimulatorPID(for: notification) { + launchScrollableSimulator(pid: pid) + } + }) + } + + private func observeDidTerminateApplication() { + didTerminateAppObserver = NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didTerminateApplicationNotification, object: nil, queue: .main, using: { [weak self] notification in + guard let self else { return } + if isSimulator(for: notification) { + scrollableSimulator?.deactivate() + } + }) + } + + private func isSimulator(for notification: Notification) -> Bool { + guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { + return false + } + return app.bundleIdentifier == SIMULATOR_BUNDLE_ID + } + + private func getSimulatorPID(for notification: Notification) -> pid_t? { + guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { + return nil + } + return app.processIdentifier + } + + private func showAlertForFailedLaunching(retryHandler: () -> Void) { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = "System failed to boot" + alert.addButton(withTitle: "Retry") + alert.addButton(withTitle: "Quit") + let response = alert.runModal() + + switch response { + case .alertFirstButtonReturn: + retryHandler() + case .alertSecondButtonReturn: + NSApplication.shared.terminate(nil) + default: + break + } + } +} + diff --git a/ScrollableSimulator/Assets.xcassets/AccentColor.colorset/Contents.json b/ScrollableSimulator/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/ScrollableSimulator/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon@16x16.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon@16x16.png new file mode 100644 index 0000000..3844763 Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon@16x16.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_128x128.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_128x128.png new file mode 100644 index 0000000..80d2c25 Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_128x128.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_128x128@2x.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_128x128@2x.png new file mode 100644 index 0000000..a1a1ade Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_128x128@2x.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_16x16@2x.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_16x16@2x.png new file mode 100644 index 0000000..5bc3ebd Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_16x16@2x.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_256x256.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_256x256.png new file mode 100644 index 0000000..606d8f4 Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_256x256.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_256x256@2x.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_256x256@2x.png new file mode 100644 index 0000000..ae5bac6 Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_256x256@2x.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_32x32.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_32x32.png new file mode 100644 index 0000000..7b8350a Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_32x32.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_32x32@2x.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_32x32@2x.png new file mode 100644 index 0000000..e57bf7a Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_32x32@2x.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_512x512.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_512x512.png new file mode 100644 index 0000000..920b0bb Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_512x512.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_512x512@2x.png b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_512x512@2x.png new file mode 100644 index 0000000..64ffc15 Binary files /dev/null and b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/App Icon_512x512@2x.png differ diff --git a/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/Contents.json b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..4cf8b7c --- /dev/null +++ b/ScrollableSimulator/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "App Icon@16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "App Icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "App Icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "App Icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "App Icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "App Icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "App Icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "App Icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "App Icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "App Icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ScrollableSimulator/Assets.xcassets/Contents.json b/ScrollableSimulator/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ScrollableSimulator/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ScrollableSimulator/Info.plist b/ScrollableSimulator/Info.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/ScrollableSimulator/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/ScrollableSimulator/Main.storyboard b/ScrollableSimulator/Main.storyboard new file mode 100644 index 0000000..32f0a67 --- /dev/null +++ b/ScrollableSimulator/Main.storyboard @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ScrollableSimulator/ScrollableSimulator.entitlements b/ScrollableSimulator/ScrollableSimulator.entitlements new file mode 100644 index 0000000..18aff0c --- /dev/null +++ b/ScrollableSimulator/ScrollableSimulator.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/ScrollableSimulator/ScrollableSimulator/MouseScrollBehavior.swift b/ScrollableSimulator/ScrollableSimulator/MouseScrollBehavior.swift new file mode 100644 index 0000000..5d18dcb --- /dev/null +++ b/ScrollableSimulator/ScrollableSimulator/MouseScrollBehavior.swift @@ -0,0 +1,48 @@ +import Foundation +import CoreGraphics + +class MouseScrollBehavior { + private let mouseScrollCompletionCaller: MouseScrollCompletionCaller = .init() + private var additionalDraggedPosition: CGPoint = .zero + + func mutateForDragging( + proxy: CGEventTapProxy, + type: CGEventType, + event mutableEvent: CGEvent, + refcon: UnsafeMutableRawPointer? + ) -> Unmanaged? { + guard let immutableEvent = mutableEvent.copy() else { return Unmanaged.passUnretained(mutableEvent) } + let targetPID = pid_t(immutableEvent.getIntegerValueField(.eventTargetUnixProcessID)) + let xScrollQuantity = getXScrollQuantity(from: immutableEvent) + let yScrollQuantity = getYScrollQuantity(from: immutableEvent) + + if !mouseScrollCompletionCaller.isInitialized() { + mouseScrollCompletionCaller.initialize(scrollCompletionHandler: { + [weak self] in + let mouseUp = makeLeftMouseUp(baseEvent: immutableEvent) + mouseUp?.postToPid(targetPID) + self?.resetState() + }) + let mouseDown = makeLeftMouseDown(baseEvent: immutableEvent) + mouseDown?.postToPid(targetPID) + } + mouseScrollCompletionCaller.send(event: immutableEvent) + + mutableEvent.type = .leftMouseDragged + additionalDraggedPosition = .init( + x: additionalDraggedPosition.x + CGFloat(xScrollQuantity), + y: additionalDraggedPosition.y + CGFloat(yScrollQuantity) + ) + mutableEvent.location = .init( + x: immutableEvent.location.x + additionalDraggedPosition.x, + y: immutableEvent.location.y + additionalDraggedPosition.y + ) + mutableEvent.postToPid(targetPID) + + return nil + } + + private func resetState() { + additionalDraggedPosition = .zero + } +} diff --git a/ScrollableSimulator/ScrollableSimulator/MouseScrollCompletionCaller.swift b/ScrollableSimulator/ScrollableSimulator/MouseScrollCompletionCaller.swift new file mode 100644 index 0000000..327c093 --- /dev/null +++ b/ScrollableSimulator/ScrollableSimulator/MouseScrollCompletionCaller.swift @@ -0,0 +1,45 @@ +import Foundation +import CoreGraphics +import Combine + +class MouseScrollCompletionCaller { + private let TIMEOUT_TIME: Double = 0.2 + private var eventQueue: [CGEvent] = [] + private var timeoutSubject: PassthroughSubject? + private var cancellable: AnyCancellable? + private var scrollCompletionHandler: () -> Void = {} + + func initialize(scrollCompletionHandler: @escaping () -> Void) { + eventQueue = [] + self.scrollCompletionHandler = scrollCompletionHandler + timeoutSubject = .init() + cancellable = timeoutSubject?.timeout( + DispatchQueue.SchedulerTimeType.Stride(floatLiteral: TIMEOUT_TIME), + scheduler: DispatchQueue.main + ) + .sink( + receiveCompletion: { [weak self] result in + self?.scrollCompletionHandler() + self?.reset() + }, + receiveValue: { _ in } + ) + } + + func send(event: CGEvent) { + guard let copyEvent = event.copy() else { return } + eventQueue.append(copyEvent) + timeoutSubject?.send() + } + + func isInitialized() -> Bool { + timeoutSubject != nil && cancellable != nil + } + + private func reset() { + eventQueue = [] + timeoutSubject = nil + cancellable = nil + scrollCompletionHandler = {} + } +} diff --git a/ScrollableSimulator/ScrollableSimulator/ScrollableSimulatorBehavior.swift b/ScrollableSimulator/ScrollableSimulator/ScrollableSimulatorBehavior.swift new file mode 100644 index 0000000..4bba4ac --- /dev/null +++ b/ScrollableSimulator/ScrollableSimulator/ScrollableSimulatorBehavior.swift @@ -0,0 +1,59 @@ +import Foundation +import CoreGraphics + +class ScrollableSimulatorBehavior { + private let eventSource = CGEventSource(stateID: .hidSystemState) + private let trackpadScrollBehavior: TrackpadScrollBehavior = .init() + private let mouseScrollBehavior: MouseScrollBehavior = .init() + + func tapEventHandler( + proxy: CGEventTapProxy, + type: CGEventType, + event: CGEvent, + refcon: UnsafeMutableRawPointer? + ) -> Unmanaged? { + switch type { + case .scrollWheel: + return eventBehaviorOnScrollWheel(proxy: proxy, type: type, event: event, refcon: refcon) + default: + return Unmanaged.passUnretained(event) + } + } + + private func eventBehaviorOnScrollWheel( + proxy: CGEventTapProxy, + type: CGEventType, + event: CGEvent, + refcon: UnsafeMutableRawPointer? + ) -> Unmanaged? { + log(scrollWheelEvent: event) + + if isContinuousScroll(from: event) { + // use trackpad + return trackpadScrollBehavior.mutateForDragging(proxy: proxy, type: type, event: event, refcon: refcon) + } else { + // use mouse + return mouseScrollBehavior.mutateForDragging(proxy: proxy, type: type, event: event, refcon: refcon) + } + } + + private func log(scrollWheelEvent: CGEvent) { + #if DEBUG + print( + String( + format: "at: %ld\tpixelY: %d\tpixelX: %d\tfixedDeltaY: %.2f\tfixedDeltaX: %.2f\tmomentum: %d\tphase: %d\tcount: %d\tinstant: %d\tisContinuous: %d", + scrollWheelEvent.timestamp, + scrollWheelEvent.getIntegerValueField(.scrollWheelEventPointDeltaAxis1), + scrollWheelEvent.getIntegerValueField(.scrollWheelEventPointDeltaAxis2), + scrollWheelEvent.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis1), + scrollWheelEvent.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis2), + scrollWheelEvent.getIntegerValueField(.scrollWheelEventMomentumPhase), + scrollWheelEvent.getIntegerValueField(.scrollWheelEventScrollPhase), + scrollWheelEvent.getIntegerValueField(.scrollWheelEventScrollCount), + scrollWheelEvent.getIntegerValueField(.scrollWheelEventInstantMouser), + scrollWheelEvent.getIntegerValueField(.scrollWheelEventIsContinuous) + ) + ) + #endif + } +} diff --git a/ScrollableSimulator/ScrollableSimulator/ScrollableSimulatorLauncher.swift b/ScrollableSimulator/ScrollableSimulator/ScrollableSimulatorLauncher.swift new file mode 100644 index 0000000..73ee0e1 --- /dev/null +++ b/ScrollableSimulator/ScrollableSimulator/ScrollableSimulatorLauncher.swift @@ -0,0 +1,55 @@ +import Foundation +import CoreGraphics + +fileprivate let behavior = ScrollableSimulatorBehavior() + +class ScrollableSimulatorLauncher { + private let runLoop: CFRunLoop = CFRunLoopGetMain() + private let runLoopMode: CFRunLoopMode = .defaultMode + private var runLoopSource: CFRunLoopSource? + + private let simulatorPID: pid_t + + init(simulatorPID: pid_t) { + self.simulatorPID = simulatorPID + } + + func activate() throws { + if let runLoopSource { + if CFRunLoopContainsSource(runLoop, runLoopSource, runLoopMode) { + CFRunLoopRemoveSource(runLoop, runLoopSource, runLoopMode) + self.runLoopSource = nil + } + } + + guard let port = CGEvent.tapCreateForPid( + pid: simulatorPID, + place: .headInsertEventTap, + options: .defaultTap, + eventsOfInterest: .max, + callback: { proxy, type, event, refcon in + behavior.tapEventHandler(proxy: proxy, type: type, event: event, refcon: refcon) + }, + userInfo: nil + ) else { + throw ScrollableSimulatorLauncherError.tapIsNotCreated + } + runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, port, 0) + CFRunLoopAddSource(runLoop, runLoopSource, runLoopMode) + CGEvent.tapEnable(tap: port, enable: true) + Logger.info("ScrollableSimulator is active!") + } + + func deactivate() { + guard let runLoopSource else { return } + if CFRunLoopContainsSource(runLoop, runLoopSource, runLoopMode) { + CFRunLoopRemoveSource(runLoop, runLoopSource, runLoopMode) + } + self.runLoopSource = nil + Logger.info("ScrollableSimulator is inactive") + } +} + +enum ScrollableSimulatorLauncherError: Error { + case tapIsNotCreated +} diff --git a/ScrollableSimulator/ScrollableSimulator/TrackpadScrollBehavior.swift b/ScrollableSimulator/ScrollableSimulator/TrackpadScrollBehavior.swift new file mode 100644 index 0000000..bd16f7a --- /dev/null +++ b/ScrollableSimulator/ScrollableSimulator/TrackpadScrollBehavior.swift @@ -0,0 +1,70 @@ +import Foundation +import CoreGraphics + +class TrackpadScrollBehavior { + private var scrollEventQueue: [CGEvent] = [] + private var additionalDraggedPosition: CGPoint = .zero + + func mutateForDragging( + proxy: CGEventTapProxy, + type: CGEventType, + event mutableEvent: CGEvent, + refcon: UnsafeMutableRawPointer? + ) -> Unmanaged? { + guard let immutableEvent = mutableEvent.copy() else { return Unmanaged.passUnretained(mutableEvent) } + + let xScrollQuantity = getXScrollQuantity(from: immutableEvent) + let yScrollQuantity = getYScrollQuantity(from: immutableEvent) + + // pattern1: began scroll + if isBeganScroll(from: immutableEvent) { + mutableEvent.type = .leftMouseDown + copyAndStoreEvent(immutableEvent) + return Unmanaged.passUnretained(mutableEvent) + } + // pattern2: end scroll(non inertial scroll) + if isEndedScroll(from: immutableEvent) && isNonInertialScroll(lastEvent: scrollEventQueue.last) { + mutableEvent.type = .leftMouseUp + resetState() + return Unmanaged.passUnretained(mutableEvent) + } + // pattern3: end scroll(inertial scroll) + if isEndContinuousScroll(from: immutableEvent) { + mutableEvent.type = .leftMouseUp + resetState() + return Unmanaged.passUnretained(mutableEvent) + } + // pattern4: while scrolling + mutableEvent.type = .leftMouseDragged + additionalDraggedPosition = .init( + x: additionalDraggedPosition.x + CGFloat(xScrollQuantity), + y: additionalDraggedPosition.y + CGFloat(yScrollQuantity) + ) + mutableEvent.location = .init( + x: immutableEvent.location.x + additionalDraggedPosition.x, + y: immutableEvent.location.y + additionalDraggedPosition.y + ) + copyAndStoreEvent(immutableEvent) + return Unmanaged.passUnretained(mutableEvent) + } + + private func isNonInertialScroll(lastEvent: CGEvent?) -> Bool { + guard let lastEvent else { + assertionFailure("lastEvent is null") + return true + } + let lastDeltaY = lastEvent.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis1) + let lastDeltaX = lastEvent.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis2) + return lastDeltaY == 0.0 && lastDeltaX == 0.0 + } + + private func copyAndStoreEvent(_ event: CGEvent) { + guard let copyEvent = event.copy() else { return } + scrollEventQueue.append(copyEvent) + } + + private func resetState() { + scrollEventQueue = [] + additionalDraggedPosition = .zero + } +} diff --git a/ScrollableSimulator/SimulatorApp.swift b/ScrollableSimulator/SimulatorApp.swift new file mode 100644 index 0000000..bc40c7f --- /dev/null +++ b/ScrollableSimulator/SimulatorApp.swift @@ -0,0 +1,11 @@ +import Cocoa + +let SIMULATOR_BUNDLE_ID = "com.apple.iphonesimulator" + +class SimulatorApp { + static func getSimulatorPID() -> pid_t? { + NSWorkspace.shared.runningApplications + .first(where: { $0.bundleIdentifier == SIMULATOR_BUNDLE_ID })? + .processIdentifier + } +} diff --git a/ScrollableSimulator/Util/CGEvent+.swift b/ScrollableSimulator/Util/CGEvent+.swift new file mode 100644 index 0000000..d444502 --- /dev/null +++ b/ScrollableSimulator/Util/CGEvent+.swift @@ -0,0 +1,93 @@ +import Foundation +import CoreGraphics + +func getYScrollQuantity(from event: CGEvent) -> Int64 { + event.getIntegerValueField(.scrollWheelEventPointDeltaAxis1) +} + +func getXScrollQuantity(from event: CGEvent) -> Int64 { + event.getIntegerValueField(.scrollWheelEventPointDeltaAxis2) +} + +func getYScrollQuantityForLineBase(from event: CGEvent) -> Int64 { + event.getIntegerValueField(.scrollWheelEventFixedPtDeltaAxis1) +} + +func getXScrollQuantityForLineBase(from event: CGEvent) -> Int64 { + event.getIntegerValueField(.scrollWheelEventFixedPtDeltaAxis2) +} + +func isBeganScroll(from event: CGEvent) -> Bool { + event.getIntegerValueField(.scrollWheelEventScrollPhase) == CGScrollPhase.began.rawValue +} + +func isEndedScroll(from event: CGEvent) -> Bool { + event.getIntegerValueField(.scrollWheelEventScrollPhase) == CGScrollPhase.ended.rawValue +} + +func isContinuousScroll(from event: CGEvent) -> Bool { + event.getIntegerValueField(.scrollWheelEventIsContinuous) != 0 +} + +func isBeginContinuousScroll(from event: CGEvent) -> Bool { + event.getIntegerValueField(.scrollWheelEventMomentumPhase) == CGMomentumScrollPhase.begin.rawValue +} + +func isEndContinuousScroll(from event: CGEvent) -> Bool { + event.getIntegerValueField(.scrollWheelEventMomentumPhase) == CGMomentumScrollPhase.end.rawValue +} + +func makeLeftMouseDown(baseEvent: CGEvent) -> CGEvent? { + guard let copyEvent = baseEvent.copy() else { return nil } + copyEvent.type = .leftMouseDown + return copyEvent +} + +func makeLeftMouseUp(baseEvent: CGEvent) -> CGEvent? { + guard let copyEvent = baseEvent.copy() else { return nil } + copyEvent.type = .leftMouseUp + return copyEvent +} + +func makeLeftMouseDragged(baseEvent: CGEvent, location: CGPoint? = nil) -> CGEvent? { + guard let copyEvent = baseEvent.copy() else { return nil } + copyEvent.type = .leftMouseDragged + if let location { + copyEvent.location = location + } + return copyEvent +} + +/// Getting scroll quantity from CGEvent.data +@available(*, deprecated, message: "Use getXScrollQuantity()") +func getXScrollQuantity(from cfData: CFData) -> Int32 { + // Find the value that marks the point where the scroll quantity is stored. + var findData: [UInt8] = [0x00, 0x01, 0x40, 0x60] // Data array in front of the scroll value (maybe) + let findCfData = CFDataCreate(kCFAllocatorDefault, &findData, findData.count) + let originRange = CFDataFind( + cfData, + findCfData, + .init(location: 0, length: CFDataGetLength(cfData)), + [] + ) + + // The value of 4 bytes behind the marker point is the scroll quantity, so extract the data at that point. + var scrollValues = [UInt8](repeating: 0, count: 4) + CFDataGetBytes( + cfData, + CFRange( + location: originRange.location + originRange.length, + length: 4 + ), + &scrollValues + ) + + // convert uint8[] -> uint32 -> int32 + let unsignedScrollQuantity = UInt32(scrollValues[0]) << 24 + + UInt32(scrollValues[1]) << 16 + + UInt32(scrollValues[2]) << 8 + + UInt32(scrollValues[3]) + let data = withUnsafeBytes(of: unsignedScrollQuantity) { Data($0) } + let signedScrollQuantity = data.withUnsafeBytes { $0.load(as: Int32.self) } + return signedScrollQuantity +} diff --git a/ScrollableSimulator/Util/Logger.swift b/ScrollableSimulator/Util/Logger.swift new file mode 100644 index 0000000..ab0cd90 --- /dev/null +++ b/ScrollableSimulator/Util/Logger.swift @@ -0,0 +1,36 @@ +import Foundation + +enum Logger { + static func info(_ items: Any?, file: String = #file, line: Int = #line, function: String = #function) { + #if DEBUG + let filename = URL(fileURLWithPath: file).lastPathComponent + if let items = items { + print("✅ [\(filename) \(line):\(function)] : \(items)") + } else { + print("✅ [\(filename) \(line):\(function)]") + } + #endif + } + + static func warn(_ items: Any?, file: String = #file, line: Int = #line, function: String = #function) { + #if DEBUG + let filename = URL(fileURLWithPath: file).lastPathComponent + if let items = items { + print("⚠️ [\(filename) \(line):\(function)] : \(items)") + } else { + print("⚠️ [\(filename) \(line):\(function)]") + } + #endif + } + + static func error(_ items: Any?, file: String = #file, line: Int = #line, function: String = #function) { + #if DEBUG + let filename = URL(fileURLWithPath: file).lastPathComponent + if let items = items { + print("⛔️ [\(filename) \(line):\(function)] : \(items)") + } else { + print("⛔️ [\(filename) \(line):\(function)]") + } + #endif + } +}