From 2af41ee64267d731b7d152c8daabeb8518cd534c Mon Sep 17 00:00:00 2001 From: Aylur Date: Mon, 30 Oct 2023 01:51:21 +0100 Subject: [PATCH] rework widget subclassing #121 --- src/utils.ts | 6 +- src/widget.ts | 71 ++++++++---- src/widgets/box.ts | 8 +- src/widgets/button.ts | 13 ++- src/widgets/centerbox.ts | 6 +- src/widgets/circularprogress.ts | 13 ++- src/widgets/entry.ts | 11 +- src/widgets/eventbox.ts | 17 ++- src/widgets/icon.ts | 6 +- src/widgets/label.ts | 6 +- src/widgets/menu.ts | 19 +++- src/widgets/overlay.ts | 9 +- src/widgets/progressbar.ts | 10 +- src/widgets/revealer.ts | 9 +- src/widgets/scrollable.ts | 13 ++- src/widgets/slider.ts | 6 +- src/widgets/stack.ts | 8 +- src/widgets/widget.ts | 189 ++++++++++++++------------------ src/widgets/window.ts | 24 ++-- 19 files changed, 255 insertions(+), 189 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 6568d0db..fc19c8ba 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -91,10 +91,10 @@ export function bulkDisconnect(service: GObject.Object, ids: number[]) { service.disconnect(id); } -export function connect( +export function connect( obj: GObject.Object, - widget: Gtk.Widget, - callback: (widget: Gtk.Widget, ...args: unknown[]) => void, + widget: T, + callback: (widget: T, ...args: unknown[]) => void, event?: string, ) { if (!(obj instanceof Service || obj instanceof App || obj instanceof Variable) && !event) diff --git a/src/widget.ts b/src/widget.ts index 263a3fdc..29b26bff 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -1,5 +1,7 @@ +/* eslint-disable max-len */ import Gtk from 'gi://Gtk?version=3.0'; -import AgsWidget from './widgets/widget.js'; +import GObject from 'gi://GObject?version=2.0'; +import AgsWidget, { type BaseProps } from './widgets/widget.js'; import AgsBox from './widgets/box.js'; import AgsCenterBox from './widgets/centerbox.js'; import AgsEventBox from './widgets/eventbox.js'; @@ -17,24 +19,27 @@ import { AgsMenu, AgsMenuItem } from './widgets/menu.js'; import AgsWindow from './widgets/window.js'; import AgsCircularProgress from './widgets/circularprogress.js'; -// @ts-expect-error margin override -export const Window = AgsWidget(AgsWindow); -export const Box = AgsWidget(AgsBox); -export const Button = AgsWidget(AgsButton); -export const CenterBox = AgsWidget(AgsCenterBox); -export const CircularProgress = AgsWidget(AgsCircularProgress); -export const Entry = AgsWidget(AgsEntry); -export const EventBox = AgsWidget(AgsEventBox); -export const Icon = AgsWidget(AgsIcon); -export const Label = AgsWidget(AgsLabel); -export const Menu = AgsWidget(AgsMenu); -export const MenuItem = AgsWidget(AgsMenuItem); -export const Overlay = AgsWidget(AgsOverlay); -export const ProgressBar = AgsWidget(AgsProgressBar); -export const Revealer = AgsWidget(AgsRevealer); -export const Scrollable = AgsWidget(AgsScrollable); -export const Slider = AgsWidget(AgsSlider); -export const Stack = AgsWidget(AgsStack); +function createCtor(Widget: T) { + return (...props: ConstructorParameters) => new Widget(...props) as InstanceType; +} + +export const Window = createCtor(AgsWindow); +export const Box = createCtor(AgsBox); +export const Button = createCtor(AgsButton); +export const CenterBox = createCtor(AgsCenterBox); +export const CircularProgress = createCtor(AgsCircularProgress); +export const Entry = createCtor(AgsEntry); +export const EventBox = createCtor(AgsEventBox); +export const Icon = createCtor(AgsIcon); +export const Label = createCtor(AgsLabel); +export const Menu = createCtor(AgsMenu); +export const MenuItem = createCtor(AgsMenuItem); +export const Overlay = createCtor(AgsOverlay); +export const ProgressBar = createCtor(AgsProgressBar); +export const Revealer = createCtor(AgsRevealer); +export const Scrollable = createCtor(AgsScrollable); +export const Slider = createCtor(AgsSlider); +export const Stack = createCtor(AgsStack); const ctors = new Map(); export function Widget< @@ -46,9 +51,9 @@ export function Widget< if (ctors.has(type)) return ctors.get(type)(props); - const ctor = AgsWidget(type); - ctors.set(type, ctor); - return ctor(props); + const Ctor = AgsWidget(type); + ctors.set(type, Ctor); + return new Ctor(props); } // so they are still accessible when importing only Widget @@ -70,4 +75,26 @@ Widget.Slider = Slider; Widget.Stack = Stack; Widget.Window = Window; +export function subclass(W: T) { + class Widget extends AgsWidget(W, `Gtk${W.name}`) { + static { GObject.registerClass({ GTypeName: `Ags${W.name}` }, this); } + constructor(props: BaseProps & Widget> & Props) { + super(props as Gtk.Widget.ConstructorProperties); + } + } + return (props: BaseProps & Widget> & Props) => new Widget(props) as InstanceType & Widget; +} + +export const Calendar = subclass(Gtk.Calendar); +export const Fixed = subclass(Gtk.Fixed); +export const MenuBar = subclass(Gtk.MenuBar); +export const Switch = subclass(Gtk.Switch); +export const ToggleButton = subclass(Gtk.ToggleButton); + +Widget.Calendar = Calendar; +Widget.Fixed = Fixed; +Widget.MenuBar = MenuBar; +Widget.Switch = Switch; +Widget.ToggleButton = ToggleButton; + export default Widget; diff --git a/src/widgets/box.ts b/src/widgets/box.ts index 8dfce9d9..b7ec9c02 100644 --- a/src/widgets/box.ts +++ b/src/widgets/box.ts @@ -1,15 +1,17 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Service from '../service.js'; -export interface BoxProps extends Gtk.Box.ConstructorProperties { +export interface BoxProps extends BaseProps, Gtk.Box.ConstructorProperties { children?: Gtk.Widget[] vertical?: boolean } -export default class AgsBox extends Gtk.Box { +export default class AgsBox extends AgsWidget(Gtk.Box) { static { GObject.registerClass({ + GTypeName: 'AgsBox', Properties: { 'vertical': Service.pspec('vertical', 'boolean', 'rw'), 'children': Service.pspec('children', 'jsobject', 'rw'), @@ -17,7 +19,7 @@ export default class AgsBox extends Gtk.Box { }, this); } - constructor({ children, ...rest }: BoxProps = {}) { + constructor({ children, ...rest }: BoxProps = {}) { super(rest); if (children) diff --git a/src/widgets/button.ts b/src/widgets/button.ts index 8d475774..a7994503 100644 --- a/src/widgets/button.ts +++ b/src/widgets/button.ts @@ -1,10 +1,11 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Gdk from 'gi://Gdk?version=3.0'; import { runCmd } from '../utils.js'; import { type Command } from './widget.js'; -export interface ButtonProps extends Gtk.Button.ConstructorProperties { +export interface ButtonProps extends BaseProps, Gtk.Button.ConstructorProperties { onClicked?: Command onPrimaryClick?: Command onSecondaryClick?: Command @@ -18,8 +19,12 @@ export interface ButtonProps extends Gtk.Button.ConstructorProperties { onScrollDown?: Command } -export default class AgsButton extends Gtk.Button { - static { GObject.registerClass(this); } +export default class AgsButton extends AgsWidget(Gtk.Button) { + static { + GObject.registerClass({ + GTypeName: 'AgsButton', + }, this); + } onClicked: Command; onPrimaryClick: Command; @@ -101,7 +106,7 @@ export default class AgsButton extends Gtk.Button { return runCmd(this.onMiddleClickRelease, this, event); }); - this.connect('scroll-event', (box, event) => { + this.connect('scroll-event', (box, event: Gdk.Event) => { if (event.get_scroll_deltas()[2] < 0) return runCmd(this.onScrollUp, box, event); else if (event.get_scroll_deltas()[2] > 0) diff --git a/src/widgets/centerbox.ts b/src/widgets/centerbox.ts index 71b1eaa7..0e4d32ad 100644 --- a/src/widgets/centerbox.ts +++ b/src/widgets/centerbox.ts @@ -1,9 +1,8 @@ import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; -import AgsBox from './box.js'; +import AgsBox, { type BoxProps } from './box.js'; -export interface CenterBoxProps extends Gtk.Box.ConstructorProperties { - children?: Gtk.Widget[] +export interface CenterBoxProps extends BoxProps { start_widget?: Gtk.Widget center_widget?: Gtk.Widget end_widget?: Gtk.Widget @@ -12,6 +11,7 @@ export interface CenterBoxProps extends Gtk.Box.ConstructorProperties { export default class AgsCenterBox extends AgsBox { static { GObject.registerClass({ + GTypeName: 'AgsCenterBox', Properties: { 'start-widget': GObject.ParamSpec.object( 'start-widget', 'Start Widget', 'Start Widget', diff --git a/src/widgets/circularprogress.ts b/src/widgets/circularprogress.ts index 8f6277d1..77dce09d 100644 --- a/src/widgets/circularprogress.ts +++ b/src/widgets/circularprogress.ts @@ -1,8 +1,8 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Service from '../service.js'; -// type from gi-types is wrong interface Context { setSourceRGBA: (r: number, g: number, b: number, a: number) => void arc: (x: number, y: number, r: number, a1: number, a2: number) => void @@ -13,16 +13,18 @@ interface Context { $dispose: () => void } -export interface CircularProgressProps extends Gtk.Bin { +export interface CircularProgressProps extends + BaseProps, Gtk.Bin.ConstructorProperties { rounded?: boolean value?: number inverted?: boolean start_at?: number } -export default class AgsCircularProgress extends Gtk.Bin { +export default class AgsCircularProgress extends AgsWidget(Gtk.Bin) { static { GObject.registerClass({ + GTypeName: 'AgsCircularProgress', CssName: 'circular-progress', Properties: { 'start-at': Service.pspec('start-at', 'float', 'rw'), @@ -33,6 +35,11 @@ export default class AgsCircularProgress extends Gtk.Bin { }, this); } + // its here for typescript to infer the type + constructor(props: CircularProgressProps) { + super(props); + } + // @ts-expect-error get rounded() { return this._rounded || false; } set rounded(r: boolean) { diff --git a/src/widgets/entry.ts b/src/widgets/entry.ts index 8dc4d880..dcc08c16 100644 --- a/src/widgets/entry.ts +++ b/src/widgets/entry.ts @@ -1,15 +1,20 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import { runCmd } from '../utils.js'; import { type Command } from './widget.js'; -export interface EntryProps extends Gtk.Entry.ConstructorProperties { +export interface EntryProps extends BaseProps, Gtk.Entry.ConstructorProperties { onAccept?: Command onChange?: Command } -export default class AgsEntry extends Gtk.Entry { - static { GObject.registerClass(this); } +export default class AgsEntry extends AgsWidget(Gtk.Entry) { + static { + GObject.registerClass({ + GTypeName: 'AgsEntry', + }, this); + } onAccept: Command; onChange: Command; diff --git a/src/widgets/eventbox.ts b/src/widgets/eventbox.ts index 0006ca29..942b4bd1 100644 --- a/src/widgets/eventbox.ts +++ b/src/widgets/eventbox.ts @@ -1,10 +1,11 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Gdk from 'gi://Gdk?version=3.0'; import { runCmd } from '../utils.js'; import { type Command } from './widget.js'; -export interface EventBoxProps extends Gtk.EventBox.ConstructorProperties { +export interface EventBoxProps extends BaseProps, Gtk.EventBox.ConstructorProperties { onPrimaryClick?: Command onSecondaryClick?: Command onMiddleClick?: Command @@ -17,8 +18,12 @@ export interface EventBoxProps extends Gtk.EventBox.ConstructorProperties { onScrollDown?: Command } -export default class AgsEventBox extends Gtk.EventBox { - static { GObject.registerClass(this); } +export default class AgsEventBox extends AgsWidget(Gtk.EventBox) { + static { + GObject.registerClass({ + GTypeName: 'AgsEventBox', + }, this); + } onPrimaryClick: Command; onSecondaryClick: Command; @@ -69,7 +74,7 @@ export default class AgsEventBox extends Gtk.EventBox { return runCmd(this.onHoverLost, box, event); }); - this.connect('button-press-event', (box, event) => { + this.connect('button-press-event', (box, event: Gdk.Event) => { box.set_state_flags(Gtk.StateFlags.ACTIVE, false); if (event.get_button()[1] === Gdk.BUTTON_PRIMARY) return runCmd(this.onPrimaryClick, box, event); @@ -81,7 +86,7 @@ export default class AgsEventBox extends Gtk.EventBox { return runCmd(this.onMiddleClick, box, event); }); - this.connect('button-release-event', (box, event) => { + this.connect('button-release-event', (box, event: Gdk.Event) => { box.unset_state_flags(Gtk.StateFlags.ACTIVE); if (event.get_button()[1] === Gdk.BUTTON_PRIMARY) return runCmd(this.onPrimaryClickRelease, box, event); @@ -93,7 +98,7 @@ export default class AgsEventBox extends Gtk.EventBox { return runCmd(this.onMiddleClickRelease, box, event); }); - this.connect('scroll-event', (box, event) => { + this.connect('scroll-event', (box, event: Gdk.Event) => { if (event.get_scroll_deltas()[2] < 0) return runCmd(this.onScrollUp, box, event); else if (event.get_scroll_deltas()[2] > 0) diff --git a/src/widgets/icon.ts b/src/widgets/icon.ts index 6bce6fa2..d759cfc2 100644 --- a/src/widgets/icon.ts +++ b/src/widgets/icon.ts @@ -1,3 +1,4 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import GLib from 'gi://GLib'; @@ -6,16 +7,17 @@ import Gdk from 'gi://Gdk?version=3.0'; import Service from '../service.js'; import cairo from '@girs/cairo-1.0'; -interface Props extends Gtk.Image.ConstructorProperties { +interface Props extends BaseProps, Gtk.Image.ConstructorProperties { icon?: string | GdkPixbuf.Pixbuf size?: number } export type IconProps = Props | string | GdkPixbuf.Pixbuf | undefined -export default class AgsIcon extends Gtk.Image { +export default class AgsIcon extends AgsWidget(Gtk.Image) { static { GObject.registerClass({ + GTypeName: 'AgsIcon', Properties: { 'icon': Service.pspec('icon', 'jsobject', 'rw'), 'size': Service.pspec('size', 'double', 'rw'), diff --git a/src/widgets/label.ts b/src/widgets/label.ts index f54d7e20..e6fae6e9 100644 --- a/src/widgets/label.ts +++ b/src/widgets/label.ts @@ -1,3 +1,4 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import GLib from 'gi://GLib'; @@ -10,16 +11,17 @@ const truncates = ['none', 'start', 'middle', 'end'] as const; type Justification = typeof justifications[number]; type Truncate = typeof truncates[number]; -interface Props extends Gtk.Label.ConstructorProperties { +interface Props extends BaseProps, Gtk.Label.ConstructorProperties { justification?: Justification truncate?: Truncate } export type LabelProps = Props | string | undefined -export default class AgsLabel extends Gtk.Label { +export default class AgsLabel extends AgsWidget(Gtk.Label) { static { GObject.registerClass({ + GTypeName: 'AgsLabel', Properties: { 'justification': Service.pspec('justification', 'string', 'rw'), 'truncate': Service.pspec('truncate', 'string', 'rw'), diff --git a/src/widgets/menu.ts b/src/widgets/menu.ts index de12d0a9..4dc77086 100644 --- a/src/widgets/menu.ts +++ b/src/widgets/menu.ts @@ -1,16 +1,21 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import { runCmd } from '../utils.js'; import { type Command } from './widget.js'; -export interface MenuProps extends Gtk.Menu.ConstructorProperties { +export interface MenuProps extends BaseProps, Gtk.Menu.ConstructorProperties { children?: Gtk.Widget[] onPopup?: Command onMoveScroll?: Command } -export class AgsMenu extends Gtk.Menu { - static { GObject.registerClass(this); } +export class AgsMenu extends AgsWidget(Gtk.Menu) { + static { + GObject.registerClass({ + GTypeName: 'AgsMenu', + }, this); + } onPopup: Command; onMoveScroll: Command; @@ -51,14 +56,18 @@ export class AgsMenu extends Gtk.Menu { } } -export interface MenuItemProps extends Gtk.Menu.ConstructorProperties { +export interface MenuItemProps extends BaseProps, Gtk.Menu.ConstructorProperties { onActivate?: Command onSelect?: Command onDeselect?: Command } export class AgsMenuItem extends Gtk.MenuItem { - static { GObject.registerClass(this); } + static { + GObject.registerClass({ + GTypeName: 'AgsMenuItem', + }, this); + } onActivate: Command; onSelect: Command; diff --git a/src/widgets/overlay.ts b/src/widgets/overlay.ts index 2f93bffb..b10582c4 100644 --- a/src/widgets/overlay.ts +++ b/src/widgets/overlay.ts @@ -1,15 +1,17 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Service from '../service.js'; -export interface OverlayProps extends Gtk.Overlay.ConstructorProperties { +export interface OverlayProps extends BaseProps, Gtk.Overlay.ConstructorProperties { pass_through?: boolean overlays?: Gtk.Widget[] } -export default class AgsOverlay extends Gtk.Overlay { +export default class AgsOverlay extends AgsWidget(Gtk.Overlay) { static { GObject.registerClass({ + GTypeName: 'AgsOverlay', Properties: { 'pass-through': Service.pspec('pass-through', 'boolean', 'rw'), 'overlays': Service.pspec('overlays', 'jsobject', 'rw'), @@ -17,6 +19,9 @@ export default class AgsOverlay extends Gtk.Overlay { }, this); } + // its for ts + constructor(props: OverlayProps) { super(props); } + get pass_through() { return this.get_children() .map(ch => this.get_overlay_pass_through(ch)) diff --git a/src/widgets/progressbar.ts b/src/widgets/progressbar.ts index 2ba2c85d..15efbc6d 100644 --- a/src/widgets/progressbar.ts +++ b/src/widgets/progressbar.ts @@ -1,15 +1,18 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Service from '../service.js'; -export interface ProgressBarProps extends Gtk.ProgressBar.ConstructorProperties { +export interface ProgressBarProps extends + BaseProps, Gtk.ProgressBar.ConstructorProperties { vertical?: boolean value?: number } -export default class AgsProgressBar extends Gtk.ProgressBar { +export default class AgsProgressBar extends AgsWidget(Gtk.ProgressBar) { static { GObject.registerClass({ + GTypeName: 'AgsProgressBar', Properties: { 'vertical': Service.pspec('vertical', 'boolean', 'rw'), 'value': Service.pspec('value', 'float', 'rw'), @@ -17,6 +20,9 @@ export default class AgsProgressBar extends Gtk.ProgressBar { }, this); } + // its for ts + constructor(props: ProgressBarProps) { super(props); } + get value() { return this.fraction; } set value(value: number) { if (this.value === value) diff --git a/src/widgets/revealer.ts b/src/widgets/revealer.ts index e8e1e162..d65432a8 100644 --- a/src/widgets/revealer.ts +++ b/src/widgets/revealer.ts @@ -1,3 +1,4 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Service from '../service.js'; @@ -10,19 +11,23 @@ const transitions = [ type Transition = typeof transitions[number]; -export interface RevealerProps extends Gtk.Revealer.ConstructorProperties { +export interface RevealerProps extends BaseProps, Gtk.Revealer.ConstructorProperties { transitions?: Transition } -export default class AgsRevealer extends Gtk.Revealer { +export default class AgsRevealer extends AgsWidget(Gtk.Revealer) { static { GObject.registerClass({ + GTypeName: 'AgsRevealer', Properties: { 'transition': Service.pspec('transition', 'string', 'rw'), }, }, this); } + // its here for ts + constructor(props: RevealerProps) { super(props); } + get transition() { return transitions[this.transition_type]; } set transition(transition: Transition) { if (!transition || this.transition === transition) diff --git a/src/widgets/scrollable.ts b/src/widgets/scrollable.ts index 408a06eb..d591a6fd 100644 --- a/src/widgets/scrollable.ts +++ b/src/widgets/scrollable.ts @@ -1,3 +1,4 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Service from '../service.js'; @@ -5,14 +6,16 @@ import Service from '../service.js'; const policy = ['automatic', 'always', 'never', 'external'] as const; type Policy = typeof policy[number] -export interface ScrollableProps extends Gtk.ScrolledWindow.ConstructorProperties { +export interface ScrollableProps extends + BaseProps, Gtk.ScrolledWindow.ConstructorProperties { hscroll?: Policy, vscroll?: Policy, } -export default class AgsScrollable extends Gtk.ScrolledWindow { +export default class AgsScrollable extends AgsWidget(Gtk.ScrolledWindow) { static { GObject.registerClass({ + GTypeName: 'AgsScrollable', Properties: { 'hscroll': Service.pspec('hscroll', 'string', 'rw'), 'vscroll': Service.pspec('vscroll', 'string', 'rw'), @@ -42,7 +45,7 @@ export default class AgsScrollable extends Gtk.ScrolledWindow { // @ts-expect-error this._hscroll = hscroll; this.notify('hscroll'); - this.policy(); + this._policy(); } // @ts-expect-error @@ -59,10 +62,10 @@ export default class AgsScrollable extends Gtk.ScrolledWindow { // @ts-expect-error this._vscroll = vscroll; this.notify('vscroll'); - this.policy(); + this._policy(); } - policy() { + private _policy() { const hscroll = policy.findIndex(p => p === this.hscroll); const vscroll = policy.findIndex(p => p === this.vscroll); this.set_policy( diff --git a/src/widgets/slider.ts b/src/widgets/slider.ts index 347e5166..6a5cf8d3 100644 --- a/src/widgets/slider.ts +++ b/src/widgets/slider.ts @@ -1,3 +1,4 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Gdk from 'gi://Gdk?version=3.0'; @@ -5,7 +6,7 @@ import { runCmd } from '../utils.js'; import { type Command } from './widget.js'; import Service from '../service.js'; -export interface SliderProps extends Gtk.Scale.ConstructorProperties { +export interface SliderProps extends BaseProps, Gtk.Scale.ConstructorProperties { onChange?: Command value?: number min?: number @@ -13,9 +14,10 @@ export interface SliderProps extends Gtk.Scale.ConstructorProperties { step?: number } -export default class AgsSlider extends Gtk.Scale { +export default class AgsSlider extends AgsWidget(Gtk.Scale) { static { GObject.registerClass({ + GTypeName: 'AgsSlider', Properties: { 'dragging': Service.pspec('dragging', 'boolean', 'r'), 'vertical': Service.pspec('vertical', 'boolean', 'rw'), diff --git a/src/widgets/stack.ts b/src/widgets/stack.ts index c18eeb96..0a753908 100644 --- a/src/widgets/stack.ts +++ b/src/widgets/stack.ts @@ -1,3 +1,4 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Service from '../service.js'; @@ -13,15 +14,16 @@ const transitions = [ type Transition = typeof transitions[number] -export interface StackProps extends Gtk.Stack.ConstructorProperties { +export interface StackProps extends BaseProps, Gtk.Stack.ConstructorProperties { shown?: string items?: [string, Gtk.Widget][] transition?: Transition } -export default class AgsStack extends Gtk.Stack { +export default class AgsStack extends AgsWidget(Gtk.Stack) { static { GObject.registerClass({ + GTypeName: 'AgsStack', Properties: { 'transition': Service.pspec('transition', 'string', 'rw'), 'shown': Service.pspec('shown', 'string', 'rw'), @@ -30,6 +32,8 @@ export default class AgsStack extends Gtk.Stack { }, this); } + constructor(props: StackProps) { super(props); } + add_named(child: Gtk.Widget, name: string): void { this.items.push([name, child]); super.add_named(child, name); diff --git a/src/widgets/widget.ts b/src/widgets/widget.ts index 3e407220..d37e0960 100644 --- a/src/widgets/widget.ts +++ b/src/widgets/widget.ts @@ -3,81 +3,65 @@ import Gtk from 'gi://Gtk?version=3.0'; import Service from '../service.js'; import { interval, connect } from '../utils.js'; -// TODO: remove this type and make them only functions -export type Command = string | ((...args: unknown[]) => boolean); +// FIXME: remove this type and make them only functions +export type Command = string | ((...args: unknown[]) => boolean | undefined); const aligns = ['fill', 'start', 'end', 'center', 'baseline'] as const; type Align = typeof aligns[number]; -export interface BaseProps { +type Connection = + [string, (self: Self, ...args: unknown[]) => unknown] | + [number, (self: Self, ...args: unknown[]) => unknown] | + [GObject.Object, (self: Self, ...args: unknown[]) => unknown, string]; + +type Property = [prop: string, value: unknown]; +type Bind = [ + prop: string, + obj: GObject.Object, + objProp?: string, + transform?: (value: unknown) => unknown, +]; + +export interface BaseProps extends Gtk.Widget.ConstructorProperties { className?: string classNames?: string[] - style?: string css?: string - halign?: Align - valign?: Align - connections?: ( - [string, (self: Self, ...args: unknown[]) => unknown] | - [number, (self: Self, ...args: unknown[]) => unknown] | - [GObject.Object, (self: Self, ...args: unknown[]) => unknown, string] - )[] - properties?: [prop: string, value: unknown][] - binds?: [ - prop: string, - obj: GObject.Object, - objProp?: string, - transform?: (value: unknown) => unknown, - ][], + hpack?: Align + vpack?: Align + connections?: Connection[] + properties?: Property[] + binds?: Bind[], setup?: (self: Self) => void } -export default function (Widget: T) { - // @ts-expect-error mixin constructor - class AgsWidget extends Widget { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type WidgetCtor = new (...args: any[]) => Gtk.Widget; +export default function (Widget: T, GTypeName?: string) { + return class AgsWidget extends Widget { static { + const pspec = (name: string) => GObject.ParamSpec.jsobject( + name, name, name, GObject.ParamFlags.CONSTRUCT_ONLY | GObject.ParamFlags.WRITABLE); + GObject.registerClass({ - GTypeName: Widget.name, + GTypeName: `Ags_${GTypeName || Widget.name}`, Properties: { 'class-name': Service.pspec('class-name', 'string', 'rw'), 'class-names': Service.pspec('class-names', 'jsobject', 'rw'), 'css': Service.pspec('css', 'string', 'rw'), + 'hpack': Service.pspec('hpack', 'string', 'rw'), + 'vpack': Service.pspec('vpack', 'string', 'rw'), + // order of these matter + 'setup': pspec('setup'), + 'properties': pspec('properties'), + 'connections': pspec('connections'), + 'binds': pspec('binds'), }, }, this); } - constructor(params: BaseProps> & ConstructorParameters[0]) { - const { - connections = [], - properties = [], - binds = [], - style, - halign, - valign, - setup, - ...rest - } = params; - super(typeof params === 'string' ? params : rest as Gtk.Widget.ConstructorProperties); - - this.style = style!; - this.halign = halign!; - this.valign = valign!; - - const widget = this as InstanceType; - - properties.forEach(([key, value]) => { - (this as unknown as { [key: string]: unknown })[`_${key}`] = value; - }); - - binds.forEach(([prop, obj, objProp = 'value', transform = out => out]) => { - if (!prop || !obj) { - console.error(Error('missing arguments to binds')); - return; - } - - // @ts-expect-error - const callback = () => this[prop] = transform(obj[objProp]); - connections.push([obj, callback, `notify::${objProp}`]); - }); + set connections(connections: Connection[]) { + if (!connections) + return; connections.forEach(([s, callback, event]) => { if (!s || !callback) { @@ -89,24 +73,50 @@ export default function (Widget: T) { this.connect(s, callback); else if (typeof s === 'number') - interval(s, () => callback(widget), widget); + interval(s, () => callback(this), this); else if (s instanceof GObject.Object) - connect(s, widget, callback as (w: Gtk.Widget) => void, event); + connect(s, this, callback, event); else console.error(Error(`${s} is not a GObject nor a string nor a number`)); }); + } + + set binds(binds: Bind[]) { + if (!binds) + return; + + binds.forEach(([prop, obj, objProp = 'value', transform = out => out]) => { + if (!prop || !obj) { + console.error(Error('missing arguments to binds')); + return; + } + + // @ts-expect-error + const callback = () => this[prop] = transform(obj[objProp]); + this.connections = [[obj, callback, `notify::${objProp}`]]; + }); + } + + set properties(properties: Property[]) { + if (!properties) + return; - if (typeof setup === 'function') - setup(widget); + properties.forEach(([key, value]) => { + (this as unknown as { [key: string]: unknown })[`_${key}`] = value; + }); } - // @ts-expect-error prop override - get halign() { return aligns[super.halign]; } + set setup(setup: (self: AgsWidget) => void) { + if (!setup) + return; + + setup(this); + } - // @ts-expect-error prop override - set halign(align: Align) { + get hpack() { return aligns[this.halign]; } + set hpack(align: Align) { if (!align) return; @@ -115,14 +125,11 @@ export default function (Widget: T) { return; } - super.halign = aligns.findIndex(a => a === align); + this.halign = aligns.findIndex(a => a === align); } - // @ts-expect-error prop override - get valign() { return aligns[super.valign]; } - - // @ts-expect-error prop override - set valign(align: Align) { + get vpack() { return aligns[this.valign]; } + set vpack(align: Align) { if (!align) return; @@ -131,7 +138,7 @@ export default function (Widget: T) { return; } - super.valign = aligns.findIndex(a => a === align); + this.valign = aligns.findIndex(a => a === align); } toggleClassName(className: string, condition = true) { @@ -161,8 +168,11 @@ export default function (Widget: T) { names.forEach(cn => this.toggleClassName(cn)); } - private _cssProvider!: Gtk.CssProvider; + _cssProvider!: Gtk.CssProvider; setCss(css: string) { + if (!css.includes('{') || !css.includes('}')) + css = `* { ${css} }`; + if (this._cssProvider) this.get_style_context().remove_provider(this._cssProvider); @@ -174,35 +184,15 @@ export default function (Widget: T) { this.notify('css'); } - setStyle(css: string) { - this.setCss(`* { ${css} }`); - this.notify('style'); - } - - // @ts-expect-error prop override - get style() { return this._style || ''; } - - // @ts-expect-error prop override - set style(css: string) { - if (!css) - return; - - // @ts-expect-error - this._style = css; - this.setCss(`* { ${css} }`); - this.notify('style'); + get css() { + return this._cssProvider.to_string() || ''; } - // @ts-expect-error - get css() { return this._css || ''; } set css(css: string) { if (!css) return; - // @ts-expect-error - this._css = css; this.setCss(css); - this.notify('css'); } get child(): Gtk.Widget | null { @@ -222,18 +212,5 @@ export default function (Widget: T) { else console.error(new Error(`can't set child on ${this}`)); } - - // @ts-expect-error prop override - get parent(): Gtk.Container | null { - return this.get_parent() as Gtk.Container || null; - } - } - - return (params: - BaseProps> & - ConstructorParameters[0] | - string, - ) => { - return new AgsWidget(params) as InstanceType; }; } diff --git a/src/widgets/window.ts b/src/widgets/window.ts index 4c4eb60b..4e504f39 100644 --- a/src/widgets/window.ts +++ b/src/widgets/window.ts @@ -1,3 +1,4 @@ +import AgsWidget, { type BaseProps } from './widget.js'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk?version=3.0'; import Gdk from 'gi://Gdk?version=3.0'; @@ -11,26 +12,27 @@ const anchors = ['left', 'right', 'top', 'bottom'] as const; type Layer = typeof layers[number] type Anchor = typeof anchors[number] -export interface WindowProps extends Omit { +export interface WindowProps extends BaseProps, Gtk.Window.ConstructorProperties { anchor?: Anchor[] exclusive?: boolean focusable?: boolean layer?: Layer - margin?: number[] + margins?: number[] monitor?: number popup?: boolean visible?: boolean } -export default class AgsWindow extends Gtk.Window { +export default class AgsWindow extends AgsWidget(Gtk.Window) { static { GObject.registerClass({ + GTypeName: 'AgsWindow', Properties: { 'anchor': Service.pspec('anchor', 'jsobject', 'rw'), 'exclusive': Service.pspec('exclusive', 'boolean', 'rw'), 'focusable': Service.pspec('focusable', 'boolean', 'rw'), 'layer': Service.pspec('layer', 'string', 'rw'), - 'margin': Service.pspec('margin', 'jsobject', 'rw'), + 'margins': Service.pspec('margins', 'jsobject', 'rw'), 'monitor': Service.pspec('monitor', 'int', 'rw'), 'popup': Service.pspec('popup', 'boolean', 'rw'), }, @@ -44,7 +46,7 @@ export default class AgsWindow extends Gtk.Window { exclusive = false, focusable = false, layer = 'top', - margin = [], + margins = [], monitor = -1, popup = false, visible = true, @@ -58,7 +60,7 @@ export default class AgsWindow extends Gtk.Window { this.exclusive = exclusive; this.focusable = focusable; this.layer = layer; - this.margin = margin; + this.margins = margins; this.monitor = monitor; this.show_all(); this.popup = popup; @@ -130,15 +132,13 @@ export default class AgsWindow extends Gtk.Window { this.notify('anchor'); } - // @ts-expect-error - get margin() { + get margins() { return ['TOP', 'RIGHT', 'BOTTOM', 'LEFT'].map(edge => LayerShell.get_margin(this, LayerShell.Edge[edge]), ); } - // @ts-expect-error - set margin(margin: number[]) { + set margins(margin: number[]) { let margins: [side: string, index: number][] = []; switch (margin.length) { case 1: @@ -162,7 +162,7 @@ export default class AgsWindow extends Gtk.Window { LayerShell.Edge[side], (margin as number[])[i]), ); - this.notify('margin'); + this.notify('margins'); } // @ts-expect-error @@ -179,7 +179,7 @@ export default class AgsWindow extends Gtk.Window { this.disconnect(this._popup); if (popup) { - this.connect('key-press-event', (_, event) => { + this.connect('key-press-event', (_, event: Gdk.Event) => { if (event.get_keyval()[1] === Gdk.KEY_Escape) { App.getWindow(this.name!) ? App.closeWindow(this.name!)