diff --git a/.github/workflows/build-test-and-docs.yml b/.github/workflows/build-test-and-docs.yml index 287d40c417..1386767cfd 100644 --- a/.github/workflows/build-test-and-docs.yml +++ b/.github/workflows/build-test-and-docs.yml @@ -52,7 +52,8 @@ jobs: swift build --target SpreadsheetExample && \ swift build --target NotesExample && \ swift build --target GtkExample && \ - swift build --target PathsExample + swift build --target PathsExample && \ + swift build --target DatePickerExample - name: Test run: swift test --test-product swift-cross-uiPackageTests @@ -106,9 +107,10 @@ jobs: buildtarget PathsExample if [ $device_type != TV ]; then - # Slider is not implemented for tvOS + # Slider and DatePicker are not implemented for tvOS buildtarget ControlsExample buildtarget RandomNumberGeneratorExample + buildtarget DatePickerExample fi if [ $device_type = iPad ]; then @@ -165,6 +167,7 @@ jobs: buildtarget PathsExample buildtarget ControlsExample buildtarget RandomNumberGeneratorExample + buildtarget DatePickerExample # TODO test whether this works on Catalyst # buildtarget SplitExample @@ -295,7 +298,8 @@ jobs: swift build --target StressTestExample && \ swift build --target SpreadsheetExample && \ swift build --target NotesExample && \ - swift build --target GtkExample + swift build --target GtkExample && \ + swift build --target DatePickerExample - name: Test run: swift test --test-product swift-cross-uiPackageTests diff --git a/Examples/Bundler.toml b/Examples/Bundler.toml index 3b96dc6128..79a006f096 100644 --- a/Examples/Bundler.toml +++ b/Examples/Bundler.toml @@ -64,3 +64,8 @@ version = '0.1.0' identifier = 'dev.swiftcrossui.HoverExample' product = 'HoverExample' version = '0.1.0' + +[apps.DatePickerExample] +identifier = 'dev.swiftcrossui.DatePickerExample' +product = 'DatePickerExample' +version = '0.1.0' diff --git a/Examples/Package.resolved b/Examples/Package.resolved index 3842945e2a..a0d340fafb 100644 --- a/Examples/Package.resolved +++ b/Examples/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "6977ba851e440a7fbdfc7cb46441e32853dc2ba48ba34fe702e6784699d08682", + "originHash" : "e48163963f1b0c0a4a505d749632d1c23f3997e66caf3ede5961e2d8b49fd2bb", "pins" : [ { "identity" : "aexml", @@ -283,7 +283,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-uwp", "state" : { - "revision" : "e3ff9c195775e16b404b82cf6886c5e81d73b6c1" + "revision" : "8128f6615b7c5b46ada289ab6d49d871ca1e13a5" } }, { @@ -291,7 +291,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-webview2core", "state" : { - "revision" : "c9911ca23455b9fcdb2429e98baa6f4d003b381c" + "revision" : "4396f5d94d6dfd1f95ab25e79de98141b7f4f183" } }, { @@ -299,7 +299,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-windowsappsdk", "state" : { - "revision" : "ba6f0ec377b70d8be835d253102ff665a0e47d99" + "revision" : "f1c50892f10c0f7f635d3c7a3d728fd634ad001a" } }, { @@ -315,7 +315,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-winui", "state" : { - "revision" : "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86" + "revision" : "42c47f4e4129c8b5a5d9912f05e1168c924ac180" } }, { @@ -401,4 +401,4 @@ } ], "version" : 3 -} +} \ No newline at end of file diff --git a/Examples/Package.swift b/Examples/Package.swift index 1735fc7675..f6446740a5 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -76,6 +76,10 @@ let package = Package( .executableTarget( name: "HoverExample", dependencies: exampleDependencies + ), + .executableTarget( + name: "DatePickerExample", + dependencies: exampleDependencies ) ] ) diff --git a/Examples/Sources/DatePickerExample/DatePickerApp.swift b/Examples/Sources/DatePickerExample/DatePickerApp.swift new file mode 100644 index 0000000000..d27562ce15 --- /dev/null +++ b/Examples/Sources/DatePickerExample/DatePickerApp.swift @@ -0,0 +1,53 @@ +import DefaultBackend +import Foundation +import SwiftCrossUI + +#if canImport(SwiftBundlerRuntime) + import SwiftBundlerRuntime +#endif + +@main +@HotReloadable +struct DatePickerApp: App { + @State var date = Date() + @State var style: DatePickerStyle? = .automatic + + var allStyles: [DatePickerStyle] + + init() { + allStyles = [.automatic] + + if #available(iOS 14, macCatalyst 14, *) { + allStyles.append(.graphical) + } + + #if !canImport(GtkBackend) + if #available(iOS 13.4, macCatalyst 13.4, *) { + allStyles.append(.compact) + #if os(iOS) || os(visionOS) || canImport(WinUIBackend) + allStyles.append(.wheel) + #endif + } + #endif + } + + var body: some Scene { + WindowGroup("Date Picker") { + VStack { + Text("Selected date: \(date)") + + Picker(of: allStyles, selection: $style) + + DatePicker( + "Test Picker", + selection: $date + ) + .datePickerStyle(style ?? .automatic) + + Button("Reset date") { + date = Date() + } + } + } + } +} diff --git a/Package.resolved b/Package.resolved index 173c2d3001..44f5f7caa3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3b87bbc3d0f0110380f592dc86a1c8c65c20f5a326f484bdbe2f6ef5e357840d", + "originHash" : "f2f19baaeb2fc5d982c6991eea69319a9a441fcad3461d5899e9f29943737a99", "pins" : [ { "identity" : "jpeg", @@ -86,7 +86,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-uwp", "state" : { - "revision" : "e3ff9c195775e16b404b82cf6886c5e81d73b6c1" + "revision" : "8128f6615b7c5b46ada289ab6d49d871ca1e13a5" } }, { @@ -94,7 +94,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-webview2core", "state" : { - "revision" : "c9911ca23455b9fcdb2429e98baa6f4d003b381c" + "revision" : "4396f5d94d6dfd1f95ab25e79de98141b7f4f183" } }, { @@ -102,7 +102,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-windowsappsdk", "state" : { - "revision" : "ba6f0ec377b70d8be835d253102ff665a0e47d99" + "revision" : "f1c50892f10c0f7f635d3c7a3d728fd634ad001a" } }, { @@ -118,7 +118,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-winui", "state" : { - "revision" : "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86" + "revision" : "42c47f4e4129c8b5a5d9912f05e1168c924ac180" } }, { @@ -141,4 +141,4 @@ } ], "version" : 3 -} +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index d854305ca1..0636fef00f 100644 --- a/Package.swift +++ b/Package.swift @@ -100,7 +100,7 @@ let package = Package( ), .package( url: "https://github.com/stackotter/swift-windowsappsdk", - revision: "ba6f0ec377b70d8be835d253102ff665a0e47d99" + revision: "f1c50892f10c0f7f635d3c7a3d728fd634ad001a" ), .package( url: "https://github.com/stackotter/swift-windowsfoundation", @@ -108,7 +108,7 @@ let package = Package( ), .package( url: "https://github.com/stackotter/swift-winui", - revision: "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86" + revision: "42c47f4e4129c8b5a5d9912f05e1168c924ac180" ), // .package( // url: "https://github.com/stackotter/TermKit", diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index f0f3fa4817..dec3c38ddb 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -349,6 +349,14 @@ public final class AppKitBackend: AppBackend { // Self.scrollBarWidth has changed action() } + + NotificationCenter.default.addObserver( + forName: .NSSystemTimeZoneDidChange, + object: nil, + queue: .main + ) { _ in + action() + } } public func computeWindowEnvironment( @@ -1689,6 +1697,80 @@ public final class AppKitBackend: AppBackend { let request = URLRequest(url: url) webView.load(request) } + + public func createDatePicker() -> NSView { + let datePicker = CustomDatePicker() + datePicker.delegate = datePicker.strongDelegate + return datePicker + } + + // Depending on the calendar, era is either necessary or must be omitted. Making the wrong + // choice for the current calendar means the cursor position is reset after every keystroke. I + // know of no simple way to tell whether NSDatePicker requires or forbids eras for a given + // calendar, so in lieu of that I have hardcoded the calendar identifiers. + private let calendarsWithEras: Set = [ + .buddhist, .coptic, .ethiopicAmeteAlem, .ethiopicAmeteMihret, .indian, .islamic, + .islamicCivil, .islamicTabular, .islamicUmmAlQura, .japanese, .persian, .republicOfChina, + ] + + public func updateDatePicker( + _ datePicker: NSView, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { + let datePicker = datePicker as! CustomDatePicker + + datePicker.isEnabled = environment.isEnabled + datePicker.textColor = environment.suggestedForegroundColor.nsColor + + // If the time zone is set to autoupdatingCurrent, then the cursor position is reset after + // every keystroke. Thanks Apple + datePicker.timeZone = + environment.timeZone == .autoupdatingCurrent ? .current : environment.timeZone + + // A couple properties cause infinite update loops if we assign to them on every update, so + // check their values first. + if datePicker.calendar != environment.calendar { + datePicker.calendar = environment.calendar + } + + if datePicker.dateValue != date { + datePicker.dateValue = date + } + + var elementFlags: NSDatePicker.ElementFlags = [] + if components.contains(.date) { + elementFlags.insert(.yearMonthDay) + if calendarsWithEras.contains(environment.calendar.identifier) { + elementFlags.insert(.era) + } + } + if components.contains(.hourMinuteAndSecond) { + elementFlags.insert(.hourMinuteSecond) + } else { + elementFlags.insert(.hourMinute) + } + + if datePicker.datePickerElements != elementFlags { + datePicker.datePickerElements = elementFlags + } + + datePicker.strongDelegate.onChange = onChange + + datePicker.minDate = range.lowerBound + datePicker.maxDate = range.upperBound + + datePicker.datePickerStyle = + switch environment.datePickerStyle { + case .automatic, .compact: + .textFieldAndStepper + case .graphical: + .clockAndCalendar + } + } } final class NSCustomTapGestureTarget: NSView { @@ -2191,3 +2273,19 @@ final class CustomWKNavigationDelegate: NSObject, WKNavigationDelegate { onNavigate?(url) } } + +final class CustomDatePicker: NSDatePicker { + var strongDelegate = CustomDatePickerDelegate() +} + +final class CustomDatePickerDelegate: NSObject, NSDatePickerCellDelegate { + var onChange: ((Date) -> Void)? + + func datePickerCell( + _: NSDatePickerCell, + validateProposedDateValue proposedDateValue: AutoreleasingUnsafeMutablePointer, + timeInterval _: UnsafeMutablePointer? + ) { + onChange?(proposedDateValue.pointee as Date) + } +} diff --git a/Sources/Gtk/Generated/Calendar.swift b/Sources/Gtk/Generated/Calendar.swift new file mode 100644 index 0000000000..5e20955cf9 --- /dev/null +++ b/Sources/Gtk/Generated/Calendar.swift @@ -0,0 +1,216 @@ +import CGtk + +/// `GtkCalendar` is a widget that displays a Gregorian calendar, one month +/// at a time. +/// +/// ![An example GtkCalendar](calendar.png) +/// +/// A `GtkCalendar` can be created with [ctor@Gtk.Calendar.new]. +/// +/// The date that is currently displayed can be altered with +/// [method@Gtk.Calendar.select_day]. +/// +/// To place a visual marker on a particular day, use +/// [method@Gtk.Calendar.mark_day] and to remove the marker, +/// [method@Gtk.Calendar.unmark_day]. Alternative, all +/// marks can be cleared with [method@Gtk.Calendar.clear_marks]. +/// +/// The selected date can be retrieved from a `GtkCalendar` using +/// [method@Gtk.Calendar.get_date]. +/// +/// Users should be aware that, although the Gregorian calendar is the +/// legal calendar in most countries, it was adopted progressively +/// between 1582 and 1929. Display before these dates is likely to be +/// historically incorrect. +/// +/// # Shortcuts and Gestures +/// +/// `GtkCalendar` supports the following gestures: +/// +/// - Scrolling up or down will switch to the previous or next month. +/// - Date strings can be dropped for setting the current day. +/// +/// # CSS nodes +/// +/// ``` +/// calendar.view +/// ├── header +/// │ ├── button +/// │ ├── stack.month +/// │ ├── button +/// │ ├── button +/// │ ├── label.year +/// │ ╰── button +/// ╰── grid +/// ╰── label[.day-name][.week-number][.day-number][.other-month][.today] +/// ``` +/// +/// `GtkCalendar` has a main node with name calendar. It contains a subnode +/// called header containing the widgets for switching between years and months. +/// +/// The grid subnode contains all day labels, including week numbers on the left +/// (marked with the .week-number css class) and day names on top (marked with the +/// .day-name css class). +/// +/// Day labels that belong to the previous or next month get the .other-month +/// style class. The label of the current day get the .today style class. +/// +/// Marked day labels get the :selected state assigned. +open class Calendar: Widget { + /// Creates a new calendar, with the current date being selected. + public convenience init() { + self.init( + gtk_calendar_new() + ) + } + + override func didMoveToParent() { + super.didMoveToParent() + + addSignal(name: "day-selected") { [weak self] () in + guard let self = self else { return } + self.daySelected?(self) + } + + addSignal(name: "next-month") { [weak self] () in + guard let self = self else { return } + self.nextMonth?(self) + } + + addSignal(name: "next-year") { [weak self] () in + guard let self = self else { return } + self.nextYear?(self) + } + + addSignal(name: "prev-month") { [weak self] () in + guard let self = self else { return } + self.prevMonth?(self) + } + + addSignal(name: "prev-year") { [weak self] () in + guard let self = self else { return } + self.prevYear?(self) + } + + let handler5: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::day", handler: gCallback(handler5)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDay?(self, param0) + } + + let handler6: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::month", handler: gCallback(handler6)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyMonth?(self, param0) + } + + let handler7: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-day-names", handler: gCallback(handler7)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowDayNames?(self, param0) + } + + let handler8: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-heading", handler: gCallback(handler8)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowHeading?(self, param0) + } + + let handler9: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-week-numbers", handler: gCallback(handler9)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowWeekNumbers?(self, param0) + } + + let handler10: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::year", handler: gCallback(handler10)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyYear?(self, param0) + } + } + + /// The selected day (as a number between 1 and 31). + @GObjectProperty(named: "day") public var day: Int + + /// The selected month (as a number between 0 and 11). + /// + /// This property gets initially set to the current month. + @GObjectProperty(named: "month") public var month: Int + + /// Determines whether day names are displayed. + @GObjectProperty(named: "show-day-names") public var showDayNames: Bool + + /// Determines whether a heading is displayed. + @GObjectProperty(named: "show-heading") public var showHeading: Bool + + /// Determines whether week numbers are displayed. + @GObjectProperty(named: "show-week-numbers") public var showWeekNumbers: Bool + + /// The selected year. + /// + /// This property gets initially set to the current year. + @GObjectProperty(named: "year") public var year: Int + + /// Emitted when the user selects a day. + public var daySelected: ((Calendar) -> Void)? + + /// Emitted when the user switched to the next month. + public var nextMonth: ((Calendar) -> Void)? + + /// Emitted when user switched to the next year. + public var nextYear: ((Calendar) -> Void)? + + /// Emitted when the user switched to the previous month. + public var prevMonth: ((Calendar) -> Void)? + + /// Emitted when user switched to the previous year. + public var prevYear: ((Calendar) -> Void)? + + public var notifyDay: ((Calendar, OpaquePointer) -> Void)? + + public var notifyMonth: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowDayNames: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowHeading: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowWeekNumbers: ((Calendar, OpaquePointer) -> Void)? + + public var notifyYear: ((Calendar, OpaquePointer) -> Void)? +} diff --git a/Sources/Gtk/Generated/SpinButton.swift b/Sources/Gtk/Generated/SpinButton.swift new file mode 100644 index 0000000000..b20ebe5a9a --- /dev/null +++ b/Sources/Gtk/Generated/SpinButton.swift @@ -0,0 +1,669 @@ +import CGtk + +/// A `GtkSpinButton` is an ideal way to allow the user to set the +/// value of some attribute. +/// +/// ![An example GtkSpinButton](spinbutton.png) +/// +/// Rather than having to directly type a number into a `GtkEntry`, +/// `GtkSpinButton` allows the user to click on one of two arrows +/// to increment or decrement the displayed value. A value can still be +/// typed in, with the bonus that it can be checked to ensure it is in a +/// given range. +/// +/// The main properties of a `GtkSpinButton` are through an adjustment. +/// See the [class@Gtk.Adjustment] documentation for more details about +/// an adjustment's properties. +/// +/// Note that `GtkSpinButton` will by default make its entry large enough +/// to accommodate the lower and upper bounds of the adjustment. If this +/// is not desired, the automatic sizing can be turned off by explicitly +/// setting [property@Gtk.Editable:width-chars] to a value != -1. +/// +/// ## Using a GtkSpinButton to get an integer +/// +/// ```c +/// // Provides a function to retrieve an integer value from a GtkSpinButton +/// // and creates a spin button to model percentage values. +/// +/// int +/// grab_int_value (GtkSpinButton *button, +/// gpointer user_data) +/// { +/// return gtk_spin_button_get_value_as_int (button); +/// } +/// +/// void +/// create_integer_spin_button (void) +/// { +/// +/// GtkWidget *window, *button; +/// GtkAdjustment *adjustment; +/// +/// adjustment = gtk_adjustment_new (50.0, 0.0, 100.0, 1.0, 5.0, 0.0); +/// +/// window = gtk_window_new (); +/// +/// // creates the spinbutton, with no decimal places +/// button = gtk_spin_button_new (adjustment, 1.0, 0); +/// gtk_window_set_child (GTK_WINDOW (window), button); +/// +/// gtk_window_present (GTK_WINDOW (window)); +/// } +/// ``` +/// +/// ## Using a GtkSpinButton to get a floating point value +/// +/// ```c +/// // Provides a function to retrieve a floating point value from a +/// // GtkSpinButton, and creates a high precision spin button. +/// +/// float +/// grab_float_value (GtkSpinButton *button, +/// gpointer user_data) +/// { +/// return gtk_spin_button_get_value (button); +/// } +/// +/// void +/// create_floating_spin_button (void) +/// { +/// GtkWidget *window, *button; +/// GtkAdjustment *adjustment; +/// +/// adjustment = gtk_adjustment_new (2.500, 0.0, 5.0, 0.001, 0.1, 0.0); +/// +/// window = gtk_window_new (); +/// +/// // creates the spinbutton, with three decimal places +/// button = gtk_spin_button_new (adjustment, 0.001, 3); +/// gtk_window_set_child (GTK_WINDOW (window), button); +/// +/// gtk_window_present (GTK_WINDOW (window)); +/// } +/// ``` +/// +/// # Shortcuts and Gestures +/// +/// The following signals have default keybindings: +/// +/// - [signal@Gtk.SpinButton::change-value] +/// +/// # CSS nodes +/// +/// ``` +/// spinbutton.horizontal +/// ├── text +/// │ ├── undershoot.left +/// │ ╰── undershoot.right +/// ├── button.down +/// ╰── button.up +/// ``` +/// +/// ``` +/// spinbutton.vertical +/// ├── button.up +/// ├── text +/// │ ├── undershoot.left +/// │ ╰── undershoot.right +/// ╰── button.down +/// ``` +/// +/// `GtkSpinButton`s main CSS node has the name spinbutton. It creates subnodes +/// for the entry and the two buttons, with these names. The button nodes have +/// the style classes .up and .down. The `GtkText` subnodes (if present) are put +/// below the text node. The orientation of the spin button is reflected in +/// the .vertical or .horizontal style class on the main node. +/// +/// # Accessibility +/// +/// `GtkSpinButton` uses the %GTK_ACCESSIBLE_ROLE_SPIN_BUTTON role. +open class SpinButton: Widget, CellEditable, Editable, Orientable { + /// Creates a new `GtkSpinButton`. + public convenience init( + adjustment: UnsafeMutablePointer!, climbRate: Double, digits: UInt + ) { + self.init( + gtk_spin_button_new(adjustment, climbRate, guint(digits)) + ) + } + + /// Creates a new `GtkSpinButton` with the given properties. + /// + /// This is a convenience constructor that allows creation + /// of a numeric `GtkSpinButton` without manually creating + /// an adjustment. The value is initially set to the minimum + /// value and a page increment of 10 * @step is the default. + /// The precision of the spin button is equivalent to the + /// precision of @step. + /// + /// Note that the way in which the precision is derived works + /// best if @step is a power of ten. If the resulting precision + /// is not suitable for your needs, use + /// [method@Gtk.SpinButton.set_digits] to correct it. + public convenience init(range min: Double, max: Double, step: Double) { + self.init( + gtk_spin_button_new_with_range(min, max, step) + ) + } + + override func didMoveToParent() { + super.didMoveToParent() + + addSignal(name: "activate") { [weak self] () in + guard let self = self else { return } + self.activate?(self) + } + + let handler1: + @convention(c) (UnsafeMutableRawPointer, GtkScrollType, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "change-value", handler: gCallback(handler1)) { + [weak self] (param0: GtkScrollType) in + guard let self = self else { return } + self.changeValue?(self, param0) + } + + let handler2: + @convention(c) (UnsafeMutableRawPointer, gpointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "input", handler: gCallback(handler2)) { [weak self] (param0: gpointer) in + guard let self = self else { return } + self.input?(self, param0) + } + + addSignal(name: "output") { [weak self] () in + guard let self = self else { return } + self.output?(self) + } + + addSignal(name: "value-changed") { [weak self] () in + guard let self = self else { return } + self.valueChanged?(self) + } + + addSignal(name: "wrapped") { [weak self] () in + guard let self = self else { return } + self.wrapped?(self) + } + + addSignal(name: "editing-done") { [weak self] () in + guard let self = self else { return } + self.editingDone?(self) + } + + addSignal(name: "remove-widget") { [weak self] () in + guard let self = self else { return } + self.removeWidget?(self) + } + + addSignal(name: "changed") { [weak self] () in + guard let self = self else { return } + self.changed?(self) + } + + let handler9: + @convention(c) (UnsafeMutableRawPointer, Int, Int, UnsafeMutableRawPointer) -> Void = + { _, value1, value2, data in + SignalBox2.run(data, value1, value2) + } + + addSignal(name: "delete-text", handler: gCallback(handler9)) { + [weak self] (param0: Int, param1: Int) in + guard let self = self else { return } + self.deleteText?(self, param0, param1) + } + + let handler10: + @convention(c) ( + UnsafeMutableRawPointer, UnsafePointer, Int, gpointer, + UnsafeMutableRawPointer + ) -> Void = + { _, value1, value2, value3, data in + SignalBox3, Int, gpointer>.run( + data, value1, value2, value3) + } + + addSignal(name: "insert-text", handler: gCallback(handler10)) { + [weak self] (param0: UnsafePointer, param1: Int, param2: gpointer) in + guard let self = self else { return } + self.insertText?(self, param0, param1, param2) + } + + let handler11: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::activates-default", handler: gCallback(handler11)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyActivatesDefault?(self, param0) + } + + let handler12: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::adjustment", handler: gCallback(handler12)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyAdjustment?(self, param0) + } + + let handler13: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::climb-rate", handler: gCallback(handler13)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyClimbRate?(self, param0) + } + + let handler14: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::digits", handler: gCallback(handler14)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDigits?(self, param0) + } + + let handler15: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::numeric", handler: gCallback(handler15)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyNumeric?(self, param0) + } + + let handler16: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::snap-to-ticks", handler: gCallback(handler16)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifySnapToTicks?(self, param0) + } + + let handler17: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::update-policy", handler: gCallback(handler17)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyUpdatePolicy?(self, param0) + } + + let handler18: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::value", handler: gCallback(handler18)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyValue?(self, param0) + } + + let handler19: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::wrap", handler: gCallback(handler19)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyWrap?(self, param0) + } + + let handler20: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::editing-canceled", handler: gCallback(handler20)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyEditingCanceled?(self, param0) + } + + let handler21: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::cursor-position", handler: gCallback(handler21)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyCursorPosition?(self, param0) + } + + let handler22: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::editable", handler: gCallback(handler22)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyEditable?(self, param0) + } + + let handler23: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::enable-undo", handler: gCallback(handler23)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyEnableUndo?(self, param0) + } + + let handler24: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::max-width-chars", handler: gCallback(handler24)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyMaxWidthChars?(self, param0) + } + + let handler25: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::selection-bound", handler: gCallback(handler25)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifySelectionBound?(self, param0) + } + + let handler26: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::text", handler: gCallback(handler26)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyText?(self, param0) + } + + let handler27: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::width-chars", handler: gCallback(handler27)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyWidthChars?(self, param0) + } + + let handler28: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::xalign", handler: gCallback(handler28)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyXalign?(self, param0) + } + + let handler29: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::orientation", handler: gCallback(handler29)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyOrientation?(self, param0) + } + } + + /// The acceleration rate when you hold down a button or key. + @GObjectProperty(named: "climb-rate") public var climbRate: Double + + /// The number of decimal places to display. + @GObjectProperty(named: "digits") public var digits: UInt + + /// Whether non-numeric characters should be ignored. + @GObjectProperty(named: "numeric") public var numeric: Bool + + /// Whether erroneous values are automatically changed to the spin buttons + /// nearest step increment. + @GObjectProperty(named: "snap-to-ticks") public var snapToTicks: Bool + + /// Whether the spin button should update always, or only when the value + /// is acceptable. + @GObjectProperty(named: "update-policy") public var updatePolicy: SpinButtonUpdatePolicy + + /// The current value. + @GObjectProperty(named: "value") public var value: Double + + /// Whether a spin button should wrap upon reaching its limits. + @GObjectProperty(named: "wrap") public var wrap: Bool + + /// The current position of the insertion cursor in chars. + @GObjectProperty(named: "cursor-position") public var cursorPosition: Int + + /// Whether the entry contents can be edited. + @GObjectProperty(named: "editable") public var editable: Bool + + /// If undo/redo should be enabled for the editable. + @GObjectProperty(named: "enable-undo") public var enableUndo: Bool + + /// The desired maximum width of the entry, in characters. + @GObjectProperty(named: "max-width-chars") public var maxWidthChars: Int + + /// The contents of the entry. + @GObjectProperty(named: "text") public var text: String + + /// Number of characters to leave space for in the entry. + @GObjectProperty(named: "width-chars") public var widthChars: Int + + /// The horizontal alignment, from 0 (left) to 1 (right). + /// + /// Reversed for RTL layouts. + @GObjectProperty(named: "xalign") public var xalign: Float + + /// The orientation of the orientable. + @GObjectProperty(named: "orientation") public var orientation: Orientation + + /// Emitted when the spin button is activated. + /// + /// The keybindings for this signal are all forms of the Enter key. + /// + /// If the Enter key results in the value being committed to the + /// spin button, then activation does not occur until Enter is + /// pressed again. + public var activate: ((SpinButton) -> Void)? + + /// Emitted when the user initiates a value change. + /// + /// This is a [keybinding signal](class.SignalAction.html). + /// + /// Applications should not connect to it, but may emit it with + /// g_signal_emit_by_name() if they need to control the cursor + /// programmatically. + /// + /// The default bindings for this signal are Up/Down and PageUp/PageDown. + public var changeValue: ((SpinButton, GtkScrollType) -> Void)? + + /// Emitted to convert the users input into a double value. + /// + /// The signal handler is expected to use [method@Gtk.Editable.get_text] + /// to retrieve the text of the spinbutton and set @new_value to the + /// new value. + /// + /// The default conversion uses g_strtod(). + public var input: ((SpinButton, gpointer) -> Void)? + + /// Emitted to tweak the formatting of the value for display. + /// + /// ```c + /// // show leading zeros + /// static gboolean + /// on_output (GtkSpinButton *spin, + /// gpointer data) + /// { + /// char *text; + /// int value; + /// + /// value = gtk_spin_button_get_value_as_int (spin); + /// text = g_strdup_printf ("%02d", value); + /// gtk_editable_set_text (GTK_EDITABLE (spin), text): + /// g_free (text); + /// + /// return TRUE; + /// } + /// ``` + public var output: ((SpinButton) -> Void)? + + /// Emitted when the value is changed. + /// + /// Also see the [signal@Gtk.SpinButton::output] signal. + public var valueChanged: ((SpinButton) -> Void)? + + /// Emitted right after the spinbutton wraps from its maximum + /// to its minimum value or vice-versa. + public var wrapped: ((SpinButton) -> Void)? + + /// This signal is a sign for the cell renderer to update its + /// value from the @cell_editable. + /// + /// Implementations of `GtkCellEditable` are responsible for + /// emitting this signal when they are done editing, e.g. + /// `GtkEntry` emits this signal when the user presses Enter. Typical things to + /// do in a handler for ::editing-done are to capture the edited value, + /// disconnect the @cell_editable from signals on the `GtkCellRenderer`, etc. + /// + /// gtk_cell_editable_editing_done() is a convenience method + /// for emitting `GtkCellEditable::editing-done`. + public var editingDone: ((SpinButton) -> Void)? + + /// This signal is meant to indicate that the cell is finished + /// editing, and the @cell_editable widget is being removed and may + /// subsequently be destroyed. + /// + /// Implementations of `GtkCellEditable` are responsible for + /// emitting this signal when they are done editing. It must + /// be emitted after the `GtkCellEditable::editing-done` signal, + /// to give the cell renderer a chance to update the cell's value + /// before the widget is removed. + /// + /// gtk_cell_editable_remove_widget() is a convenience method + /// for emitting `GtkCellEditable::remove-widget`. + public var removeWidget: ((SpinButton) -> Void)? + + /// Emitted at the end of a single user-visible operation on the + /// contents. + /// + /// E.g., a paste operation that replaces the contents of the + /// selection will cause only one signal emission (even though it + /// is implemented by first deleting the selection, then inserting + /// the new content, and may cause multiple ::notify::text signals + /// to be emitted). + public var changed: ((SpinButton) -> Void)? + + /// Emitted when text is deleted from the widget by the user. + /// + /// The default handler for this signal will normally be responsible for + /// deleting the text, so by connecting to this signal and then stopping + /// the signal with g_signal_stop_emission(), it is possible to modify the + /// range of deleted text, or prevent it from being deleted entirely. + /// + /// The @start_pos and @end_pos parameters are interpreted as for + /// [method@Gtk.Editable.delete_text]. + public var deleteText: ((SpinButton, Int, Int) -> Void)? + + /// Emitted when text is inserted into the widget by the user. + /// + /// The default handler for this signal will normally be responsible + /// for inserting the text, so by connecting to this signal and then + /// stopping the signal with g_signal_stop_emission(), it is possible + /// to modify the inserted text, or prevent it from being inserted entirely. + public var insertText: ((SpinButton, UnsafePointer, Int, gpointer) -> Void)? + + public var notifyActivatesDefault: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyAdjustment: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyClimbRate: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyDigits: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyNumeric: ((SpinButton, OpaquePointer) -> Void)? + + public var notifySnapToTicks: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyUpdatePolicy: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyValue: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyWrap: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyEditingCanceled: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyCursorPosition: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyEditable: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyEnableUndo: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyMaxWidthChars: ((SpinButton, OpaquePointer) -> Void)? + + public var notifySelectionBound: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyText: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyWidthChars: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyXalign: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyOrientation: ((SpinButton, OpaquePointer) -> Void)? +} diff --git a/Sources/Gtk/Utility/GDateTime.swift b/Sources/Gtk/Utility/GDateTime.swift new file mode 100644 index 0000000000..a4d61cb666 --- /dev/null +++ b/Sources/Gtk/Utility/GDateTime.swift @@ -0,0 +1,55 @@ +import CGtk +import Foundation + +public class GDateTime { + public let pointer: OpaquePointer + + public init(_ pointer: OpaquePointer) { + self.pointer = pointer + } + + public init?(_ pointer: OpaquePointer?) { + guard let pointer else { return nil } + self.pointer = pointer + } + + public convenience init?(unixEpoch: TimeInterval) { + // g_date_time_new_from_unix_utc_usec appears to be too new + self.init(g_date_time_new_from_unix_utc(gint64(unixEpoch))) + } + + public convenience init?( + timeZone: GTimeZone, + year: Int, + month: Int, + day: Int, + hour: Int, + minute: Int, + second: Double + ) { + self.init( + g_date_time_new( + timeZone.pointer, + gint(year), + gint(month), + gint(day), + gint(hour), + gint(minute), + second + ) + ) + } + + public convenience init!(_ date: Date) { + self.init(unixEpoch: date.timeIntervalSince1970) + } + + deinit { + g_date_time_unref(pointer) + } + + public func toDate() -> Date { + let offset = g_date_time_to_unix(pointer) + return Date(timeIntervalSince1970: Double(offset)) + } +} diff --git a/Sources/Gtk/Utility/GTimeZone.swift b/Sources/Gtk/Utility/GTimeZone.swift new file mode 100644 index 0000000000..7190315e07 --- /dev/null +++ b/Sources/Gtk/Utility/GTimeZone.swift @@ -0,0 +1,19 @@ +import CGtk +import Foundation + +public final class GTimeZone { + public let pointer: OpaquePointer + + public init?(identifier: String) { + guard let pointer = g_time_zone_new_identifier(identifier) else { return nil } + self.pointer = pointer + } + + public convenience init?(_ timeZone: TimeZone) { + self.init(identifier: timeZone.identifier) + } + + deinit { + g_time_zone_unref(pointer) + } +} diff --git a/Sources/Gtk/Widgets/Box.swift b/Sources/Gtk/Widgets/Box.swift index 2111d4f1a2..73f2aa2abe 100644 --- a/Sources/Gtk/Widgets/Box.swift +++ b/Sources/Gtk/Widgets/Box.swift @@ -39,6 +39,10 @@ open class Box: Widget, Orientable { children = [] } + public func insert(child: Widget, after sibling: Widget) { + gtk_box_insert_child_after(castedPointer(), child.widgetPointer, sibling.widgetPointer) + } + @GObjectProperty(named: "spacing") open var spacing: Int @GObjectProperty(named: "orientation") open var orientation: Orientation diff --git a/Sources/Gtk/Widgets/Calendar+ManualAdditions.swift b/Sources/Gtk/Widgets/Calendar+ManualAdditions.swift new file mode 100644 index 0000000000..bfc400c1dc --- /dev/null +++ b/Sources/Gtk/Widgets/Calendar+ManualAdditions.swift @@ -0,0 +1,15 @@ +import CGtk +import Foundation + +extension Calendar { + public var date: Date { + get { + GDateTime(gtk_calendar_get_date(opaquePointer)).toDate() + } + set { + withExtendedLifetime(GDateTime(newValue)) { gDateTime in + gtk_calendar_select_day(opaquePointer, gDateTime.pointer) + } + } + } +} diff --git a/Sources/Gtk/Widgets/Calendar.swift b/Sources/Gtk/Widgets/Calendar.swift deleted file mode 100644 index de8215a136..0000000000 --- a/Sources/Gtk/Widgets/Calendar.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2016 Tomas Linhart. All rights reserved. -// - -import CGtk - -public class Calendar: Widget { - public convenience init() { - self.init( - gtk_calendar_new() - ) - } - - /// The selected year. This property gets initially set to the current year. - @GObjectProperty(named: "year") public var year: Int - /// Determines whether a heading is displayed. - @GObjectProperty(named: "show-heading") public var showHeading: Bool -} diff --git a/Sources/Gtk/Widgets/SpinButton+ManualAdditions.swift b/Sources/Gtk/Widgets/SpinButton+ManualAdditions.swift new file mode 100644 index 0000000000..1980000d06 --- /dev/null +++ b/Sources/Gtk/Widgets/SpinButton+ManualAdditions.swift @@ -0,0 +1,7 @@ +import CGtk + +extension SpinButton { + public func setRange(min: Double, max: Double) { + gtk_spin_button_set_range(opaquePointer, min, max) + } +} diff --git a/Sources/Gtk3/Generated/Calendar.swift b/Sources/Gtk3/Generated/Calendar.swift new file mode 100644 index 0000000000..0d23f2d51c --- /dev/null +++ b/Sources/Gtk3/Generated/Calendar.swift @@ -0,0 +1,232 @@ +import CGtk3 + +/// #GtkCalendar is a widget that displays a Gregorian calendar, one month +/// at a time. It can be created with gtk_calendar_new(). +/// +/// The month and year currently displayed can be altered with +/// gtk_calendar_select_month(). The exact day can be selected from the +/// displayed month using gtk_calendar_select_day(). +/// +/// To place a visual marker on a particular day, use gtk_calendar_mark_day() +/// and to remove the marker, gtk_calendar_unmark_day(). Alternative, all +/// marks can be cleared with gtk_calendar_clear_marks(). +/// +/// The way in which the calendar itself is displayed can be altered using +/// gtk_calendar_set_display_options(). +/// +/// The selected date can be retrieved from a #GtkCalendar using +/// gtk_calendar_get_date(). +/// +/// Users should be aware that, although the Gregorian calendar is the +/// legal calendar in most countries, it was adopted progressively +/// between 1582 and 1929. Display before these dates is likely to be +/// historically incorrect. +open class Calendar: Widget { + /// Creates a new calendar, with the current date being selected. + public convenience init() { + self.init( + gtk_calendar_new() + ) + } + + override func didMoveToParent() { + super.didMoveToParent() + + addSignal(name: "day-selected") { [weak self] () in + guard let self = self else { return } + self.daySelected?(self) + } + + addSignal(name: "day-selected-double-click") { [weak self] () in + guard let self = self else { return } + self.daySelectedDoubleClick?(self) + } + + addSignal(name: "month-changed") { [weak self] () in + guard let self = self else { return } + self.monthChanged?(self) + } + + addSignal(name: "next-month") { [weak self] () in + guard let self = self else { return } + self.nextMonth?(self) + } + + addSignal(name: "next-year") { [weak self] () in + guard let self = self else { return } + self.nextYear?(self) + } + + addSignal(name: "prev-month") { [weak self] () in + guard let self = self else { return } + self.prevMonth?(self) + } + + addSignal(name: "prev-year") { [weak self] () in + guard let self = self else { return } + self.prevYear?(self) + } + + let handler7: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::day", handler: gCallback(handler7)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDay?(self, param0) + } + + let handler8: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::detail-height-rows", handler: gCallback(handler8)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDetailHeightRows?(self, param0) + } + + let handler9: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::detail-width-chars", handler: gCallback(handler9)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDetailWidthChars?(self, param0) + } + + let handler10: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::month", handler: gCallback(handler10)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyMonth?(self, param0) + } + + let handler11: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::no-month-change", handler: gCallback(handler11)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyNoMonthChange?(self, param0) + } + + let handler12: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-day-names", handler: gCallback(handler12)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowDayNames?(self, param0) + } + + let handler13: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-details", handler: gCallback(handler13)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowDetails?(self, param0) + } + + let handler14: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-heading", handler: gCallback(handler14)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowHeading?(self, param0) + } + + let handler15: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-week-numbers", handler: gCallback(handler15)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowWeekNumbers?(self, param0) + } + + let handler16: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::year", handler: gCallback(handler16)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyYear?(self, param0) + } + } + + /// Emitted when the user selects a day. + public var daySelected: ((Calendar) -> Void)? + + /// Emitted when the user double-clicks a day. + public var daySelectedDoubleClick: ((Calendar) -> Void)? + + /// Emitted when the user clicks a button to change the selected month on a + /// calendar. + public var monthChanged: ((Calendar) -> Void)? + + /// Emitted when the user switched to the next month. + public var nextMonth: ((Calendar) -> Void)? + + /// Emitted when user switched to the next year. + public var nextYear: ((Calendar) -> Void)? + + /// Emitted when the user switched to the previous month. + public var prevMonth: ((Calendar) -> Void)? + + /// Emitted when user switched to the previous year. + public var prevYear: ((Calendar) -> Void)? + + public var notifyDay: ((Calendar, OpaquePointer) -> Void)? + + public var notifyDetailHeightRows: ((Calendar, OpaquePointer) -> Void)? + + public var notifyDetailWidthChars: ((Calendar, OpaquePointer) -> Void)? + + public var notifyMonth: ((Calendar, OpaquePointer) -> Void)? + + public var notifyNoMonthChange: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowDayNames: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowDetails: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowHeading: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowWeekNumbers: ((Calendar, OpaquePointer) -> Void)? + + public var notifyYear: ((Calendar, OpaquePointer) -> Void)? +} diff --git a/Sources/Gtk3/Generated/SpinButton.swift b/Sources/Gtk3/Generated/SpinButton.swift new file mode 100644 index 0000000000..0e07eafef1 --- /dev/null +++ b/Sources/Gtk3/Generated/SpinButton.swift @@ -0,0 +1,363 @@ +import CGtk3 + +/// A #GtkSpinButton is an ideal way to allow the user to set the value of +/// some attribute. Rather than having to directly type a number into a +/// #GtkEntry, GtkSpinButton allows the user to click on one of two arrows +/// to increment or decrement the displayed value. A value can still be +/// typed in, with the bonus that it can be checked to ensure it is in a +/// given range. +/// +/// The main properties of a GtkSpinButton are through an adjustment. +/// See the #GtkAdjustment section for more details about an adjustment's +/// properties. Note that GtkSpinButton will by default make its entry +/// large enough to accomodate the lower and upper bounds of the adjustment, +/// which can lead to surprising results. Best practice is to set both +/// the #GtkEntry:width-chars and #GtkEntry:max-width-chars poperties +/// to the desired number of characters to display in the entry. +/// +/// # CSS nodes +/// +/// |[ +/// spinbutton.horizontal +/// ├── undershoot.left +/// ├── undershoot.right +/// ├── entry +/// │ ╰── ... +/// ├── button.down +/// ╰── button.up +/// ]| +/// +/// |[ +/// spinbutton.vertical +/// ├── undershoot.left +/// ├── undershoot.right +/// ├── button.up +/// ├── entry +/// │ ╰── ... +/// ╰── button.down +/// ]| +/// +/// GtkSpinButtons main CSS node has the name spinbutton. It creates subnodes +/// for the entry and the two buttons, with these names. The button nodes have +/// the style classes .up and .down. The GtkEntry subnodes (if present) are put +/// below the entry node. The orientation of the spin button is reflected in +/// the .vertical or .horizontal style class on the main node. +/// +/// ## Using a GtkSpinButton to get an integer +/// +/// |[ +/// // Provides a function to retrieve an integer value from a GtkSpinButton +/// // and creates a spin button to model percentage values. +/// +/// gint +/// grab_int_value (GtkSpinButton *button, +/// gpointer user_data) +/// { +/// return gtk_spin_button_get_value_as_int (button); +/// } +/// +/// void +/// create_integer_spin_button (void) +/// { +/// +/// GtkWidget *window, *button; +/// GtkAdjustment *adjustment; +/// +/// adjustment = gtk_adjustment_new (50.0, 0.0, 100.0, 1.0, 5.0, 0.0); +/// +/// window = gtk_window_new (GTK_WINDOW_TOPLEVEL); +/// gtk_container_set_border_width (GTK_CONTAINER (window), 5); +/// +/// // creates the spinbutton, with no decimal places +/// button = gtk_spin_button_new (adjustment, 1.0, 0); +/// gtk_container_add (GTK_CONTAINER (window), button); +/// +/// gtk_widget_show_all (window); +/// } +/// ]| +/// +/// ## Using a GtkSpinButton to get a floating point value +/// +/// |[ +/// // Provides a function to retrieve a floating point value from a +/// // GtkSpinButton, and creates a high precision spin button. +/// +/// gfloat +/// grab_float_value (GtkSpinButton *button, +/// gpointer user_data) +/// { +/// return gtk_spin_button_get_value (button); +/// } +/// +/// void +/// create_floating_spin_button (void) +/// { +/// GtkWidget *window, *button; +/// GtkAdjustment *adjustment; +/// +/// adjustment = gtk_adjustment_new (2.500, 0.0, 5.0, 0.001, 0.1, 0.0); +/// +/// window = gtk_window_new (GTK_WINDOW_TOPLEVEL); +/// gtk_container_set_border_width (GTK_CONTAINER (window), 5); +/// +/// // creates the spinbutton, with three decimal places +/// button = gtk_spin_button_new (adjustment, 0.001, 3); +/// gtk_container_add (GTK_CONTAINER (window), button); +/// +/// gtk_widget_show_all (window); +/// } +/// ]| +open class SpinButton: Entry, Orientable { + /// Creates a new #GtkSpinButton. + public convenience init( + adjustment: UnsafeMutablePointer!, climbRate: Double, digits: UInt + ) { + self.init( + gtk_spin_button_new(adjustment, climbRate, guint(digits)) + ) + } + + /// This is a convenience constructor that allows creation of a numeric + /// #GtkSpinButton without manually creating an adjustment. The value is + /// initially set to the minimum value and a page increment of 10 * @step + /// is the default. The precision of the spin button is equivalent to the + /// precision of @step. + /// + /// Note that the way in which the precision is derived works best if @step + /// is a power of ten. If the resulting precision is not suitable for your + /// needs, use gtk_spin_button_set_digits() to correct it. + public convenience init(range min: Double, max: Double, step: Double) { + self.init( + gtk_spin_button_new_with_range(min, max, step) + ) + } + + override func didMoveToParent() { + super.didMoveToParent() + + let handler0: + @convention(c) (UnsafeMutableRawPointer, GtkScrollType, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "change-value", handler: gCallback(handler0)) { + [weak self] (param0: GtkScrollType) in + guard let self = self else { return } + self.changeValue?(self, param0) + } + + let handler1: + @convention(c) (UnsafeMutableRawPointer, gpointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "input", handler: gCallback(handler1)) { [weak self] (param0: gpointer) in + guard let self = self else { return } + self.input?(self, param0) + } + + addSignal(name: "output") { [weak self] () in + guard let self = self else { return } + self.output?(self) + } + + addSignal(name: "value-changed") { [weak self] () in + guard let self = self else { return } + self.valueChanged?(self) + } + + addSignal(name: "wrapped") { [weak self] () in + guard let self = self else { return } + self.wrapped?(self) + } + + let handler5: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::adjustment", handler: gCallback(handler5)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyAdjustment?(self, param0) + } + + let handler6: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::climb-rate", handler: gCallback(handler6)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyClimbRate?(self, param0) + } + + let handler7: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::digits", handler: gCallback(handler7)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDigits?(self, param0) + } + + let handler8: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::numeric", handler: gCallback(handler8)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyNumeric?(self, param0) + } + + let handler9: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::snap-to-ticks", handler: gCallback(handler9)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifySnapToTicks?(self, param0) + } + + let handler10: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::update-policy", handler: gCallback(handler10)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyUpdatePolicy?(self, param0) + } + + let handler11: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::value", handler: gCallback(handler11)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyValue?(self, param0) + } + + let handler12: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::wrap", handler: gCallback(handler12)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyWrap?(self, param0) + } + + let handler13: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::orientation", handler: gCallback(handler13)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyOrientation?(self, param0) + } + } + + @GObjectProperty(named: "digits") public var digits: UInt + + @GObjectProperty(named: "numeric") public var numeric: Bool + + @GObjectProperty(named: "snap-to-ticks") public var snapToTicks: Bool + + @GObjectProperty(named: "update-policy") public var updatePolicy: SpinButtonUpdatePolicy + + @GObjectProperty(named: "value") public var value: Double + + @GObjectProperty(named: "wrap") public var wrap: Bool + + /// The ::change-value signal is a [keybinding signal][GtkBindingSignal] + /// which gets emitted when the user initiates a value change. + /// + /// Applications should not connect to it, but may emit it with + /// g_signal_emit_by_name() if they need to control the cursor + /// programmatically. + /// + /// The default bindings for this signal are Up/Down and PageUp and/PageDown. + public var changeValue: ((SpinButton, GtkScrollType) -> Void)? + + /// The ::input signal can be used to influence the conversion of + /// the users input into a double value. The signal handler is + /// expected to use gtk_entry_get_text() to retrieve the text of + /// the entry and set @new_value to the new value. + /// + /// The default conversion uses g_strtod(). + public var input: ((SpinButton, gpointer) -> Void)? + + /// The ::output signal can be used to change to formatting + /// of the value that is displayed in the spin buttons entry. + /// |[ + /// // show leading zeros + /// static gboolean + /// on_output (GtkSpinButton *spin, + /// gpointer data) + /// { + /// GtkAdjustment *adjustment; + /// gchar *text; + /// int value; + /// + /// adjustment = gtk_spin_button_get_adjustment (spin); + /// value = (int)gtk_adjustment_get_value (adjustment); + /// text = g_strdup_printf ("%02d", value); + /// gtk_entry_set_text (GTK_ENTRY (spin), text); + /// g_free (text); + /// + /// return TRUE; + /// } + /// ]| + public var output: ((SpinButton) -> Void)? + + /// The ::value-changed signal is emitted when the value represented by + /// @spinbutton changes. Also see the #GtkSpinButton::output signal. + public var valueChanged: ((SpinButton) -> Void)? + + /// The ::wrapped signal is emitted right after the spinbutton wraps + /// from its maximum to minimum value or vice-versa. + public var wrapped: ((SpinButton) -> Void)? + + public var notifyAdjustment: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyClimbRate: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyDigits: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyNumeric: ((SpinButton, OpaquePointer) -> Void)? + + public var notifySnapToTicks: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyUpdatePolicy: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyValue: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyWrap: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyOrientation: ((SpinButton, OpaquePointer) -> Void)? +} diff --git a/Sources/Gtk3/Widgets/Calendar.swift b/Sources/Gtk3/Widgets/Calendar.swift deleted file mode 100644 index d1141f6b35..0000000000 --- a/Sources/Gtk3/Widgets/Calendar.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright © 2016 Tomas Linhart. All rights reserved. -// - -import CGtk3 - -public class Calendar: Widget { - public convenience init() { - self.init(gtk_calendar_new()) - } - - /// The selected year. This property gets initially set to the current year. - @GObjectProperty(named: "year") public var year: Int - /// Determines whether a heading is displayed. - @GObjectProperty(named: "show-heading") public var showHeading: Bool -} diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 418014c1c0..2753cb9141 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1500,6 +1500,37 @@ public final class GtkBackend: AppBackend { } } + public func createDatePicker() -> Widget { + let widget = Gtk.Calendar() + widget.date = Date() + return widget + } + + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { + if components.contains(.hourAndMinute) { + print("Warning: time picker is unimplemented on GtkBackend") + } + if environment.datePickerStyle != .automatic && environment.datePickerStyle != .graphical { + print("Warning: only DatePickerStyle.graphical is implemented in GtkBackend") + } + + let calendarWidget = datePicker as! Gtk.Calendar + calendarWidget.date = date + calendarWidget.daySelected = { calendarWidget in + onChange(calendarWidget.date) + } + calendarWidget.sensitive = environment.isEnabled + calendarWidget.css.clear() + calendarWidget.css.set(properties: Self.cssProperties(for: environment, isControl: true)) + } + // MARK: Helpers private func wrapInCustomRootContainer(_ widget: Widget) -> Widget { @@ -1581,3 +1612,169 @@ extension UnsafeMutablePointer { class CustomListBox: ListBox { var cachedSelection: Int? = nil } + +// This kinda sorta works. Beyond the fact that it never shows the AM/PM picker, the SpinButtons +// don't behave correctly on change, and calendar.date(bySetting:value:of:) doesn't do what we need +// it to do. +@available(macOS 13, *) +final class TimePicker: Box { + private var hourCycle: Locale.HourCycle + private let hourPicker: SpinButton + private let hourMinuteSeparator = Label(string: ":") + private let minutePicker = SpinButton(range: 0, max: 59, step: 1) + private var minuteSecondSeparator: Label? + private var secondPicker: SpinButton? + private var amPmPicker: DropDown? + + var onChange: ((Date) -> Void)? + + init() { + let hourCycle = Locale.current.hourCycle + + self.hourCycle = hourCycle + self.hourPicker = SpinButton( + range: TimePicker.minHour(for: hourCycle), + max: TimePicker.maxHour(for: hourCycle), + step: 1 + ) + + super.init(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)) + + self.hourPicker.wrap = true + self.hourPicker.orientation = .vertical + self.hourPicker.numeric = true + self.minutePicker.wrap = true + self.minutePicker.orientation = .vertical + self.minutePicker.numeric = true + + self.add(self.hourPicker) + self.add(self.hourMinuteSeparator) + self.add(self.minutePicker) + } + + func setEnabled(to isEnabled: Bool) { + hourPicker.sensitive = isEnabled + } + + private static func minHour(for hourCycle: Locale.HourCycle) -> Double { + switch hourCycle { + case .zeroToEleven, .zeroToTwentyThree: 0 + case .oneToTwelve, .oneToTwentyFour: 1 + #if os(macOS) + @unknown default: fatalError() + #endif + } + } + + private static func maxHour(for hourCycle: Locale.HourCycle) -> Double { + switch hourCycle { + case .zeroToEleven: 11 + case .oneToTwelve: 12 + case .zeroToTwentyThree: 23 + case .oneToTwentyFour: 24 + #if os(macOS) + @unknown default: fatalError() + #endif + } + } + + func update(calendar: Foundation.Calendar, date: Date, showSeconds: Bool) { + let components = calendar.dateComponents([.hour, .minute, .second], from: date) + + if showSeconds { + let secondsRange = calendar.range(of: .second, in: .minute, for: date) ?? 0..<60 + if let secondPicker { + secondPicker.setRange( + min: Double(secondsRange.lowerBound), + max: Double(secondsRange.upperBound - 1) + ) + } else { + minuteSecondSeparator = Label(string: ":") + secondPicker = SpinButton( + range: Double(secondsRange.lowerBound), + max: Double(secondsRange.upperBound - 1), + step: 1 + ) + secondPicker!.numeric = true + secondPicker!.wrap = true + secondPicker!.text = "\(components.second!)" + insert(child: minuteSecondSeparator!, after: minutePicker) + insert(child: secondPicker!, after: minuteSecondSeparator!) + } + } else { + if let minuteSecondSeparator { + remove(minuteSecondSeparator) + self.minuteSecondSeparator = nil + } + if let secondPicker { + remove(secondPicker) + self.secondPicker = nil + } + } + + let minutesRange = calendar.range(of: .minute, in: .hour, for: date) ?? 0..<60 + minutePicker.setRange( + min: Double(minutesRange.lowerBound), + max: Double(minutesRange.upperBound - 1) + ) + minutePicker.text = "\(components.minute!)" + minutePicker.valueChanged = { [unowned self] minutePicker in + guard let value = Int(exactly: minutePicker.value), + let newDate = calendar.date(bySetting: .minute, value: value, of: date) + else { + return + } + self.onChange?(newDate) + } + + let hoursRange = calendar.range(of: .hour, in: .day, for: date) + self.hourCycle = (calendar.locale ?? .current).hourCycle + let effectiveHours = hoursRange?.map { + TimePicker.transformToRange($0, hourCycle: self.hourCycle) + } + + hourPicker.setRange( + min: effectiveHours?.min().map(Double.init(_:)) + ?? TimePicker.minHour(for: self.hourCycle), + max: effectiveHours?.max().map(Double.init(_:)) + ?? TimePicker.maxHour(for: self.hourCycle) + ) + + if self.hourCycle == .oneToTwelve || self.hourCycle == .zeroToEleven { + if let amPmPicker { + // update strings if necessary + } else { + amPmPicker = DropDown(strings: [calendar.amSymbol, calendar.pmSymbol]) + add(amPmPicker!) + } + } else { + if let amPmPicker { + remove(amPmPicker) + self.amPmPicker = nil + } + } + + hourPicker.text = + "\(TimePicker.transformToRange(components.hour!, hourCycle: self.hourCycle))" + hourPicker.valueChanged = { [unowned self] hourPicker in + guard let value = Int(exactly: hourPicker.value), + let newDate = calendar.date(bySetting: .hour, value: value, of: date) + else { + return + } + self.onChange?(newDate) + } + } + + private static func transformToRange(_ value: Int, hourCycle: Locale.HourCycle) -> Int { + switch hourCycle { + case .zeroToEleven: value % 12 + case .oneToTwelve: (value + 11) % 12 + 1 + case .zeroToTwentyThree: value % 24 + case .oneToTwentyFour: (value + 23) % 24 + 1 + #if os(macOS) + @unknown default: fatalError() + #endif + } + } +} diff --git a/Sources/GtkCodeGen/GtkCodeGen.swift b/Sources/GtkCodeGen/GtkCodeGen.swift index 921f604305..ecf30734d8 100644 --- a/Sources/GtkCodeGen/GtkCodeGen.swift +++ b/Sources/GtkCodeGen/GtkCodeGen.swift @@ -27,6 +27,12 @@ struct GtkCodeGen { "GtkSelectionModel*": "OpaquePointer?", "GtkListItemFactory*": "OpaquePointer?", "GtkTextTagTable*": "OpaquePointer?", + "int": "Int", + ] + + static let cTypesManuallyConverted: [String: String] = [ + "guint": "guint", + "int": "CInt", ] /// Problematic signals which are excluded from the generated Swift @@ -110,7 +116,7 @@ struct GtkCodeGen { "Button", "Entry", "Label", "Range", "Scale", "Image", "Switch", "Spinner", "ProgressBar", "FileChooserNative", "NativeDialog", "GestureClick", "GestureSingle", "Gesture", "EventController", "GestureLongPress", "GLArea", "DrawingArea", - "CheckButton", + "CheckButton", "Calendar", "SpinButton", ] let gtk3AllowListedClasses = ["MenuShell", "EventBox"] let gtk4AllowListedClasses = [ @@ -805,6 +811,10 @@ struct GtkCodeGen { .unsafeCopy() .baseAddress! """ + } else if let type = parameter.type?.cType, + let destinationType = cTypesManuallyConverted[type] + { + return "\(destinationType)(\(argument))" } return argument diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 31a412cf2f..f0fede9cc0 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -538,6 +538,19 @@ public protocol AppBackend: Sendable { /// Sets the index of the selected option of a picker. func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) + func createDatePicker() -> Widget + + #if !os(tvOS) + func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) + #endif + /// Creates an indeterminate progress spinner. func createProgressSpinner() -> Widget @@ -1162,4 +1175,17 @@ extension AppBackend { ) { todo() } + + public func createDatePicker() -> Widget { todo() } + + #if !os(tvOS) + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { todo() } + #endif } diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index ffb03d5d94..80e70c7d6f 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -190,6 +190,16 @@ public struct EnvironmentValues { ) } + /// The current calendar that views should use when handling dates. + public var calendar: Calendar + + /// The current time zone that views should use when handling dates. + public var timeZone: TimeZone + + #if !os(tvOS) + public var datePickerStyle: DatePickerStyle + #endif + /// Creates the default environment. init(backend: Backend) { self.backend = backend @@ -212,6 +222,11 @@ public struct EnvironmentValues { isEnabled = true scrollDismissesKeyboardMode = .automatic isTextSelectionEnabled = false + calendar = .autoupdatingCurrent + timeZone = .autoupdatingCurrent + #if !os(tvOS) + datePickerStyle = .automatic + #endif } /// Returns a copy of the environment with the specified property set to the diff --git a/Sources/SwiftCrossUI/Views/DatePicker.swift b/Sources/SwiftCrossUI/Views/DatePicker.swift new file mode 100644 index 0000000000..7d4a7dd2d1 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/DatePicker.swift @@ -0,0 +1,152 @@ +import Foundation + +@available(tvOS, unavailable) +public struct DatePickerComponents: OptionSet, Sendable { + public let rawValue: UInt + + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let date = DatePickerComponents(rawValue: 0x1C) + public static let hourAndMinute = DatePickerComponents(rawValue: 0x60) + + @available(iOS, unavailable) + @available(visionOS, unavailable) + @available(macCatalyst, unavailable) + public static let hourMinuteAndSecond = DatePickerComponents(rawValue: 0xE0) +} + +@available(tvOS, unavailable) +public enum DatePickerStyle: Sendable, Hashable { + /// A date input chosen by the backend. + case automatic + + /// A date input that shows a calendar grid. + @available(iOS 14, macCatalyst 14, *) + case graphical + + /// A smaller date input. This may be a text field, or a button that opens a calendar pop-up. + @available(iOS 13.4, macCatalyst 13.4, *) + case compact + + /// A set of scrollable inputs that can be used to select a date. + @available(iOS 13.4, macCatalyst 13.4, *) + @available(macOS, unavailable) + case wheel +} + +@available(tvOS, unavailable) +public struct DatePicker { + private var label: Label + private var selection: Binding + private var range: ClosedRange + private var components: DatePickerComponents + private var style: DatePickerStyle = .automatic + + /// Displays a date input. + /// - Parameters: + /// - selection: The currently-selected date. + /// - range: The range of dates to display. The backend takes this as a hint but it is not + /// necessarily enforced. As such this parameter should be treated as an aid to validation + /// rather than a replacement for it. + /// - displayedComponents: What parts of the date/time to display in the input. + /// - label: The view to be shown next to the date input. + public nonisolated init( + selection: Binding, + range: ClosedRange = Date.distantPast...Date.distantFuture, + displayedComponents: DatePickerComponents = [.hourAndMinute, .date], + @ViewBuilder label: () -> Label + ) { + self.label = label() + self.selection = selection + self.range = range + self.components = displayedComponents + } + + /// Displays a date input. + /// - Parameters: + /// - label: The text to be shown next to the date input. + /// - selection: The currently-selected date. + /// - range: The range of dates to display. The backend takes this as a hint but it is not + /// necessarily enforced. As such this parameter should be treated as an aid to validation + /// rather than a replacement for it. + /// - displayedComponents: What parts of the date/time to display in the input. + public nonisolated init( + _ label: String, + selection: Binding, + range: ClosedRange = Date.distantPast...Date.distantFuture, + displayedComponents: DatePickerComponents = [.hourAndMinute, .date] + ) where Label == Text { + self.label = Text(label) + self.selection = selection + self.range = range + self.components = displayedComponents + } + + public typealias Components = DatePickerComponents +} + +@available(tvOS, unavailable) +extension DatePicker: View { + public var body: some View { + HStack { + label + + DatePickerImplementation(selection: selection, range: range, components: components) + } + } +} + +@available(tvOS, unavailable) +internal struct DatePickerImplementation: ElementaryView { + @Binding private var selection: Date + private var range: ClosedRange + private var components: DatePickerComponents + + init(selection: Binding, range: ClosedRange, components: DatePickerComponents) { + self._selection = selection + self.range = range + self.components = components + } + + let body = EmptyView() + + func asWidget(backend: Backend) -> Backend.Widget { + backend.createDatePicker() + } + + func update( + _ widget: Backend.Widget, + proposedSize: SIMD2, + environment: EnvironmentValues, + backend: Backend, + dryRun: Bool + ) -> ViewUpdateResult { + #if os(tvOS) + preconditionFailure() + #else + if !dryRun { + backend.updateDatePicker( + widget, + environment: environment, + date: selection, + range: range, + components: components, + onChange: { selection = $0 } + ) + } + + // I reject your proposedSize and substitute my own + let naturalSize = backend.naturalSize(of: widget) + if !dryRun { + backend.setSize(of: widget, to: naturalSize) + } + return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize)) + #endif + } +} diff --git a/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift new file mode 100644 index 0000000000..6b1407da13 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift @@ -0,0 +1,13 @@ +extension View { + @available(tvOS, unavailable) + public func datePickerStyle(_ style: DatePickerStyle) -> some View { + #if os(tvOS) + assertionFailure() + return EmptyView() + #else + EnvironmentModifier(self) { environment in + environment.with(\.datePickerStyle, style) + } + #endif + } +} diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index 238a97bdc1..6c31e865e4 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -239,6 +239,26 @@ final class SliderWidget: WrapperWidget { } } +@available(tvOS, unavailable) +final class DatePickerWidget: WrapperWidget { + var onChange: ((Date) -> Void)? { + didSet { + if oldValue == nil { + child.addTarget(self, action: #selector(dateChanged), for: .valueChanged) + } + } + } + + @objc + func dateChanged(sender: UIDatePicker) { + onChange?(sender.date) + } + + override var intrinsicContentSize: CGSize { + return child.sizeThatFits(UIView.layoutFittingCompressedSize) + } +} + extension UIKitBackend { public func createButton() -> Widget { ButtonWidget() @@ -501,5 +521,59 @@ extension UIKitBackend { let sliderWidget = slider as! SliderWidget sliderWidget.child.setValue(Float(value), animated: true) } + + public func createDatePicker() -> Widget { + DatePickerWidget() + } + + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { + let datePickerWidget = datePicker as! DatePickerWidget + + datePickerWidget.child.date = date + datePickerWidget.onChange = onChange + + datePickerWidget.child.isEnabled = environment.isEnabled + datePickerWidget.child.calendar = environment.calendar + datePickerWidget.child.timeZone = environment.timeZone + datePickerWidget.child.minimumDate = range.lowerBound + datePickerWidget.child.maximumDate = range.upperBound + + datePickerWidget.child.datePickerMode = + switch components { + case [.date, .hourAndMinute]: + .dateAndTime + case .date: + .date + case .hourAndMinute: + .time + default: + // Crashing upon receiving [] is consistent with SwiftUI. + fatalError("Unexpected Components: \(components)") + } + + if #available(iOS 13.4, macCatalyst 13.4, *) { + switch environment.datePickerStyle { + case .automatic: + datePickerWidget.child.preferredDatePickerStyle = .automatic + case .compact: + datePickerWidget.child.preferredDatePickerStyle = .compact + case .graphical: + guard #available(iOS 14, macCatalyst 14, *) else { + preconditionFailure( + "DatePickerStyle.graphical is only available on iOS 14 or newer") + } + datePickerWidget.child.preferredDatePickerStyle = .inline + case .wheel: + datePickerWidget.child.preferredDatePickerStyle = .wheels + } + } + } #endif } diff --git a/Sources/UIKitBackend/UIKitBackend.swift b/Sources/UIKitBackend/UIKitBackend.swift index f7580a6a06..0e385649fe 100644 --- a/Sources/UIKitBackend/UIKitBackend.swift +++ b/Sources/UIKitBackend/UIKitBackend.swift @@ -10,6 +10,8 @@ public final class UIKitBackend: AppBackend { static var mainWindow: UIWindow? static var hasReturnedAWindow = false + private var timeZoneObserver: NSObjectProtocol? + public let scrollBarWidth = 0 public let defaultPaddingAmount = 15 public let requiresToggleSwitchSpacer = true @@ -87,6 +89,7 @@ public final class UIKitBackend: AppBackend { var environment = defaultEnvironment environment.toggleStyle = .switch + environment.timeZone = .current switch UITraitCollection.current.userInterfaceStyle { case .light: @@ -102,6 +105,17 @@ public final class UIKitBackend: AppBackend { public func setRootEnvironmentChangeHandler(to action: @escaping () -> Void) { onTraitCollectionChange = action + if timeZoneObserver == nil { + timeZoneObserver = NotificationCenter.default.addObserver( + forName: .NSSystemTimeZoneDidChange, + object: nil, + queue: .main + ) { [unowned self] _ in + MainActor.assumeIsolated { + self.onTraitCollectionChange?() + } + } + } } public func computeWindowEnvironment( diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 882285118a..cd408336d1 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -463,6 +463,16 @@ public final class WinUIBackend: AppBackend { // the defaults set in the following code from the WinUI repository: // https://github.com/marcelwgn/microsoft-ui-xaml/blob/ff21f9b212cea2191b959649e45e52486c8465aa/src/controls/dev/ProgressRing/ProgressRing.xaml#L12 return SIMD2(32, 32) + } else if let datePicker = widget as? CustomDatePicker { + // CustomDatePicker is a StackPanel whose individual subviews need to be manually sized + // and then added together. Its naturalSize(in:) method dispatches back here once for + // each of its children. + return datePicker.naturalSize(in: self) + } else if widget is WinUI.DatePicker { + // Width is 296: + // https://github.com/marcelwgn/microsoft-ui-xaml/blob/ff21f9b212cea2191b959649e45e52486c8465aa/src/controls/dev/CommonStyles/DatePicker_themeresources.xaml#L261 + // Height is experimentally 29 which I don't see anywhere in that file. + return SIMD2(296, 29) } let oldWidth = widget.width @@ -526,6 +536,17 @@ public final class WinUIBackend: AppBackend { 64, 32 ) + } else if widget is CalendarView { + // I don't actually know why this is necessary, but without it the abbreviations for the + // weekdays wrap, making it taller than it says it is. Value was derived by trial and + // error. + adjustment = SIMD2(20, 0) + } else if computedSize.width == 0 && computedSize.width == 0 && widget is CalendarDatePicker + { + // I can't find any source on what the size of CalendarDatePicker is, but it reports 0x0 + // in at least some cases before initial render. In these cases, use a size derived + // experimentally. + adjustment = SIMD2(116, 32) } else { adjustment = .zero } @@ -1704,6 +1725,57 @@ public final class WinUIBackend: AppBackend { winUiPath.data = path.group } + public func createDatePicker() -> Widget { + return CustomDatePicker() + } + + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { + let customDatePicker = datePicker as! CustomDatePicker + + if components.contains(.hourMinuteAndSecond) { + print( + "DatePickerComponents.hourMinuteAndSecond is not supported in WinUIBackend. Falling back to .hourAndMinute." + ) + } + + customDatePicker.toggleTimeView(shown: components.contains(.hourAndMinute)) + + if environment.timeZone != .autoupdatingCurrent { + print("environment.timeZone is has no effect in WinUIBackend.") + } + + let dateViewType: CustomDatePicker.DateViewType.Discriminator? = + if components.contains(.date) { + switch environment.datePickerStyle { + case .automatic, .wheel: + .datePicker + case .compact: + .calendarDatePicker + case .graphical: + .calendarView + } + } else { + nil + } + + customDatePicker.onChange = onChange + customDatePicker.changeDateView(to: dateViewType) + customDatePicker.updateIfNeeded(date: date, calendar: environment.calendar) + customDatePicker.setDateRange(to: range) + customDatePicker.setEnabled(to: environment.isEnabled) + + // TODO(parity): foreground color ignored + // Setting foreground like for other views works for TimePicker and DatePicker but not for + // CalendarView or CalendarDatePicker. + } + // public func createTable(rows: Int, columns: Int) -> Widget { // let grid = Grid() // grid.columnSpacing = 10 @@ -1940,3 +2012,276 @@ public final class GeometryGroupHolder { var group = GeometryGroup() var strokeStyle: StrokeStyle? } + +@MainActor +final class CustomDatePicker: StackPanel { + override init() { + super.init() + self.spacing = 10 + } + + deinit { + timeChangedEvent?.dispose() + dateChangedEvent?.dispose() + } + + enum DateViewType { + case calendarView(CalendarView) + case calendarDatePicker(CalendarDatePicker) + case datePicker(WinUI.DatePicker) + + var asControl: Control { + switch self { + case .calendarView(let calendarView): calendarView + case .calendarDatePicker(let calendarDatePicker): calendarDatePicker + case .datePicker(let datePicker): datePicker + } + } + + enum Discriminator { + case calendarView, calendarDatePicker, datePicker + } + + var discriminator: Discriminator { + switch self { + case .calendarView(_): .calendarView + case .calendarDatePicker(_): .calendarDatePicker + case .datePicker(_): .datePicker + } + } + } + + private var dateView: DateViewType? + private var timeView: TimePicker? + private var date = Date() + private var calendar = Calendar.current + private var needsUpdate = false + var onChange: ((Date) -> Void)? + private var timeChangedEvent: EventCleanup? + private var dateChangedEvent: EventCleanup? + + func toggleTimeView(shown: Bool) { + guard shown != (self.timeView != nil) else { return } + + if shown { + let timeView = TimePicker() + children.append(timeView) + self.timeView = timeView + timeChangedEvent = timeView.timeChanged.addHandler { [unowned self] _, change in + guard let change else { return } + self.date = + calendar.startOfDay(for: date) + + Double(change.newTime.duration) / ticksPerSecond + self.onChange?(self.date) + } + needsUpdate = true + } else { + timeChangedEvent?.dispose() + timeChangedEvent = nil + children.removeAtEnd() + self.timeView = nil + } + } + + func setEnabled(to isEnabled: Bool) { + dateView?.asControl.isEnabled = isEnabled + timeView?.isEnabled = isEnabled + } + + func changeDateView(to newDiscriminator: DateViewType.Discriminator?) { + guard newDiscriminator != dateView?.discriminator else { return } + + dateChangedEvent?.dispose() + if dateView != nil { + children.removeAt(0) + } + + switch newDiscriminator { + case .calendarView: + let calendarView = CalendarView() + dateView = .calendarView(calendarView) + children.insertAt(0, calendarView) + orientation = .vertical + dateChangedEvent = calendarView.selectedDatesChanged.addHandler { + [unowned self] _, _ in + + guard calendarView.selectedDates.size > 0 else { return } + + self.date = componentsToFoundationDate( + dateTime: calendarView.selectedDates.getAt(0), + timeSpan: timeView?.selectedTime + ) + + if calendarView.selectedDates.size > 1 { + self.needsUpdate = true + } + + self.onChange?(self.date) + } + needsUpdate = true + case .calendarDatePicker: + let calendarDatePicker = CalendarDatePicker() + dateView = .calendarDatePicker(calendarDatePicker) + children.insertAt(0, calendarDatePicker) + orientation = .horizontal + dateChangedEvent = calendarDatePicker.dateChanged.addHandler { + [unowned self] _, change in + + guard let newDate = change?.newDate else { return } + self.date = componentsToFoundationDate( + dateTime: newDate, timeSpan: timeView?.selectedTime) + self.onChange?(self.date) + } + needsUpdate = true + case .datePicker: + let datePicker = WinUI.DatePicker() + dateView = .datePicker(datePicker) + children.insertAt(0, datePicker) + orientation = .horizontal + dateChangedEvent = datePicker.selectedDateChanged.addHandler { + [unowned self] _, _ in + + guard let selectedDate = datePicker.selectedDate else { return } + self.date = componentsToFoundationDate( + dateTime: selectedDate, timeSpan: timeView?.selectedTime) + self.onChange?(self.date) + } + needsUpdate = true + case nil: + break + } + } + + func setDateRange(to range: ClosedRange) { + guard let dateView else { return } + + let (startDate, _) = foundationDateToComponents(range.lowerBound) + let (endDate, _) = foundationDateToComponents(range.upperBound) + + switch dateView { + case .calendarView(let calendarView): + calendarView.minDate = startDate + calendarView.maxDate = endDate + case .calendarDatePicker(let calendarDatePicker): + calendarDatePicker.minDate = startDate + calendarDatePicker.maxDate = endDate + case .datePicker(let datePicker): + datePicker.minYear = startDate + datePicker.maxYear = endDate + } + } + + func updateIfNeeded(date: Date, calendar: Calendar) { + if !needsUpdate && date == self.date && calendar == self.calendar { return } + defer { needsUpdate = false } + + self.date = date + self.calendar = calendar + + let (dateTime, timeSpan) = foundationDateToComponents(date) + + switch dateView { + case .calendarView(let calendarView): + calendarView.calendarIdentifier = identifier(for: calendar) + switch calendarView.selectedDates.size { + case 0: + calendarView.selectedDates.append(dateTime) + case 1: + calendarView.selectedDates.setAt(0, dateTime) + default: + calendarView.selectedDates.clear() + calendarView.selectedDates.setAt(0, dateTime) + } + case .calendarDatePicker(let calendarDatePicker): + calendarDatePicker.calendarIdentifier = identifier(for: calendar) + calendarDatePicker.date = dateTime + case .datePicker(let datePicker): + datePicker.selectedDate = dateTime + case nil: + break + } + + if let timeView { + timeView.selectedTime = timeSpan + } + } + + private func identifier(for calendar: Calendar) -> String { + switch calendar.identifier { + case .chinese: "ChineseLunarCalendar" + case .gregorian, .iso8601: "GregorianCalendar" + case .hebrew: "HebrewCalendar" + case .islamicTabular: "HijriCalendar" + case .islamicUmmAlQura: "UmAlQuraCalendar" + case .japanese: "JapaneseCalendar" + case .persian: "PersianCalendar" + case .republicOfChina: "TaiwanCalendar" + #if compiler(>=6.2) + case .vietnamese: "VietnameseLunarCalendar" + #endif + case let id: fatalError("Unsupported calendar identifier \(id)") + } + } + + // Magic numbers taken from https://stackoverflow.com/a/5471380/6253337 + private let ticksPerSecond: Double = 10_000_000 + private let unixEpochInUniversalTime: Int64 = 116_444_736_000_000_000 + + private func foundationDateToComponents(_ date: Date) -> (DateTime, TimeSpan) { + let timeInterval = date.timeIntervalSince(calendar.startOfDay(for: date)) + + return ( + DateTime( + universalTime: Int64( + date.timeIntervalSince1970 * ticksPerSecond + Double(unixEpochInUniversalTime) + ) + ), + TimeSpan(duration: Int64(timeInterval * ticksPerSecond)) + ) + } + + private func componentsToFoundationDate(dateTime: DateTime, timeSpan: TimeSpan?) -> Date { + let baseDate = Date( + timeIntervalSince1970: Double(dateTime.universalTime - unixEpochInUniversalTime) + / ticksPerSecond + ) + + if let timeSpan { + let time = Double(timeSpan.duration) / ticksPerSecond + return calendar.startOfDay(for: baseDate) + time + } else { + return baseDate + } + } + + func naturalSize(in backend: WinUIBackend) -> SIMD2 { + let timeViewSize = + if timeView != nil { + // Width is 242, as shown in the WinUI repository: + // https://github.com/marcelwgn/microsoft-ui-xaml/blob/ff21f9b212cea2191b959649e45e52486c8465aa/src/controls/dev/CommonStyles/TimePicker_themeresources.xaml#L116 + // Height is experimentally 29 which I don't see anywhere in that file. + SIMD2(242, 29) + } else { + SIMD2.zero + } + + let dateViewSize = + if let dateControl = dateView?.asControl { + backend.naturalSize(of: dateControl) + } else { + SIMD2.zero + } + + if orientation == .horizontal { + return SIMD2( + x: timeViewSize.x + dateViewSize.x + Int(self.spacing), + y: max(timeViewSize.y, dateViewSize.y) + ) + } else { + return SIMD2( + x: max(timeViewSize.x, dateViewSize.x), + y: timeViewSize.y + dateViewSize.y + Int(self.spacing) + ) + } + } +}