refactor how widgets are subclassed (#153)

* rework widget subclassing #121

* remove Utils.connect, add Widget.bind, Widget.connectTo

* add: types for on_event fields

* bump version to 1.5.1

* export service classes and named instance constants

* add starter config example
This commit is contained in:
Aylur
2023-11-06 00:58:19 +01:00
committed by GitHub
parent 7720f98f1a
commit 11414ffef2
39 changed files with 976 additions and 664 deletions

View File

@@ -0,0 +1,27 @@
import { Variable } from 'resource:///com/github/Aylur/ags/variable.js';
import { Widget } from 'resource:///com/github/Aylur/ags/widget.js';
const time = new Variable('', {
poll: [1000, 'date'],
});
const Bar = (/** @type {number} */ monitor) => Widget.Window({
monitor,
name: `bar${monitor}`,
anchor: ['top', 'left', 'right'],
exclusive: true,
child: Widget.CenterBox({
start_widget: Widget.Label({
hpack: 'center',
label: 'Welcome to AGS!',
}),
end_widget: Widget.Label({
hpack: 'center',
binds: [['label', time]],
}),
})
})
export default {
windows: [Bar(0)]
}

View File

@@ -0,0 +1,21 @@
{
"name": "ags-config",
"version": "1.0.0",
"description": "AGS starter configuration",
"main": "config.js",
"author": "Aylur",
"repository": {
"type": "git",
"url": "git+https://github.com/Aylur/ags.git"
},
"devDependencies": {
"@girs/dbusmenugtk3-0.4": "^0.4.0-3.2.0",
"@girs/gobject-2.0": "^2.76.1-3.2.3",
"@girs/gtk-3.0": "^3.24.39-3.2.2",
"@girs/gvc-1.0": "^1.0.0-3.1.0",
"@girs/nm-1.0": "^1.43.1-3.1.0",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"eslint": "^8.44.0"
}
}

85
example/starter-config/setup.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
dir="${1:-$HOME/.config/ags}"
mkdir -p /tmp/ags-config
cd /tmp/ags-config
# clone
echo "cloning ags"
git clone https://github.com/Aylur/ags.git
cd ags
npm i
# generate
echo "generating types..."
tsc -d --declarationDir dts --emitDeclarationOnly
# fix paths
find ./dts -type f | xargs sed -i 's/gi:\/\/Gtk?version=3.0/node_modules\/@girs\/gtk-3.0\/gtk-3.0/g'
find ./dts -type f | xargs sed -i 's/gi:\/\/GObject/node_modules\/@girs\/gobject-2.0\/gobject-2.0/g'
find ./dts -type f | xargs sed -i 's/gi:\/\/Gio/node_modules\/@girs\/gio-2.0\/gio-2.0/g'
find ./dts -type f | xargs sed -i 's/gi:\/\/GLib/node_modules\/@girs\/glib-2.0\/glib-2.0/g'
find ./dts -type f | xargs sed -i 's/gi:\/\/GdkPixbuf/node_modules\/@girs\/gdkpixbuf-2.0\/gdkpixbuf-2.0/g'
find ./dts -type f | xargs sed -i 's/gi:\/\/Gdk/node_modules\/@girs\/gdk-2.0\/gdk-2.0/g'
find ./dts -type f | xargs sed -i 's/gi:\/\/Gvc/node_modules\/@girs\/gvc-1.0\/gvc-1.0/g'
find ./dts -type f | xargs sed -i 's/gi:\/\/NM/node_modules\/@girs\/nm-1.0\/nm-1.0/g'
find ./dts -type f | xargs sed -i 's/gi:\/\/DbusmenuGtk3/node_modules\/@girs\/dbusmenugtk3-0.4\/dbusmenugtk3-0.4/g'
# move
mv dts $dir/types
echo "types moved to $dir/types"
# gen ags.d.ts
function mod {
echo "declare module '$1' {
const exports: typeof import('$2')
export = exports
}"
}
function resource {
mod "resource:///com/github/Aylur/ags/$1.js" "./$1"
}
function gi {
mod "gi://$1" "node_modules/@girs/$2/$2"
}
dts="$dir/types/ags.d.ts"
echo "
declare function print(...args: any[]): void;
declare module console {
export function error(obj: object, others?: object[]): void;
export function error(msg: string, subsitutions?: any[]): void;
export function log(obj: object, others?: object[]): void;
export function log(msg: string, subsitutions?: any[]): void;
export function warn(obj: object, others?: object[]): void;
export function warn(msg: string, subsitutions?: any[]): void;
}
" >$dts
for file in ./src/*.ts; do
f=$(basename -s .ts $file)
if [[ "$f" != "main" && "$f" != "client" ]]; then
resource "$(basename -s .ts $file)" >>$dts
fi
done
for file in ./src/service/*.ts; do
resource "service/$(basename -s .ts $file)" >>$dts
done
for file in ./src/widgets/*.ts; do
resource "widgets/$(basename -s .ts $file)" >>$dts
done
# remove tmp
rm -rf /tmp/ags-config
# npm i
echo "npm install @girs"
cd $dir
npm i

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022"
],
"allowJs": true,
"checkJs": true,
"strict": true,
"noImplicitAny": false,
"baseUrl": ".",
"typeRoots": [
"./types/ags.d.ts",
"./node_modules/@girs"
],
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@@ -1,5 +1,5 @@
project('ags', 'c',
version: '1.5.0',
version: '1.5.1',
meson_version: '>= 0.62.0',
license: ['GPL-3.0-or-later'],
default_options: [ 'warning_level=2', 'werror=false', ],

View File

@@ -33,7 +33,7 @@ let
in
stdenv.mkDerivation {
pname = "ags";
version = "1.5.0";
version = "1.5.1";
src = buildNpmPackage {
name = "ags";
@@ -41,7 +41,7 @@ stdenv.mkDerivation {
dontBuild = true;
npmDepsHash = "sha256-+Hg4wEnJrMcs0m0hosDF8+UbhKNGSIcl5NcvAsM6U2Q=";
npmDepsHash = "sha256-LSx7cvtQffAMA4jHGUmuvhnUOY2PAu0VGW8OHHd67WY=";
installPhase = ''
mkdir $out

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ags",
"version": "1.5.0",
"version": "1.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ags",
"version": "1.5.0",
"version": "1.5.1",
"license": "GPL",
"dependencies": {
"typescript": "^5.1.0"

View File

@@ -1,6 +1,6 @@
{
"name": "ags",
"version": "1.5.0",
"version": "1.5.1",
"description": "description",
"main": "src/main.ts",
"repository": "",

View File

@@ -5,7 +5,7 @@ import { CACHE_DIR, ensureDirectory, readFile, writeFile } from '../utils.js';
const APPS_CACHE_DIR = `${CACHE_DIR}/apps`;
const CACHE_FILE = APPS_CACHE_DIR + '/apps_frequency.json';
class Application extends Service {
export class Application extends Service {
static {
Service.register(this, {}, {
'app': ['jsobject'],
@@ -71,7 +71,7 @@ class Application extends Service {
}
}
class Applications extends Service {
export class Applications extends Service {
static {
Service.register(this, {}, {
'list': ['jsobject'],
@@ -134,4 +134,5 @@ class Applications extends Service {
}
}
export default new Applications();
export const applications = new Applications;
export default applications;

View File

@@ -11,7 +11,7 @@ const _MIXER_CONTROL_STATE = {
[Gvc.MixerControlState.FAILED]: 'failed',
};
class Stream extends Service {
export class Stream extends Service {
static {
Service.register(this, {
'closed': [],
@@ -67,7 +67,7 @@ class Stream extends Service {
}
get volume() {
const max = audioService.control.get_vol_max_norm();
const max = audio.control.get_vol_max_norm();
return this._stream.volume / max;
}
@@ -78,7 +78,7 @@ class Stream extends Service {
if (value < 0)
value = 0;
const max = audioService.control.get_vol_max_norm();
const max = audio.control.get_vol_max_norm();
this._stream.set_volume(value * max);
this._stream.push_volume();
}
@@ -89,7 +89,7 @@ class Stream extends Service {
}
}
class Audio extends Service {
export class Audio extends Service {
static {
Service.register(this, {
'speaker-changed': [],
@@ -225,5 +225,5 @@ class Audio extends Service {
}
}
const audioService = new Audio();
export default audioService;
const audio = new Audio;
export default audio;

View File

@@ -12,7 +12,7 @@ const DeviceState = {
FULLY_CHARGED: 4,
};
class Battery extends Service {
export class Battery extends Service {
static {
Service.register(this, {
'closed': [],
@@ -103,4 +103,5 @@ class Battery extends Service {
}
}
export default new Battery();
export const battery = new Battery;
export default battery;

View File

@@ -13,7 +13,7 @@ const _ADAPTER_STATE = {
[GnomeBluetooth.AdapterState.OFF]: 'off',
};
class BluetoothDevice extends Service {
export class BluetoothDevice extends Service {
static {
Service.register(this, {}, {
'address': ['string'],
@@ -78,7 +78,7 @@ class BluetoothDevice extends Service {
setConnection(connect: boolean) {
this._connecting = true;
bluetoothService.connectDevice(this, connect, () => {
bluetooth.connectDevice(this, connect, () => {
this._connecting = false;
this.changed('connecting');
});
@@ -86,7 +86,7 @@ class BluetoothDevice extends Service {
}
}
class Bluetooth extends Service {
export class Bluetooth extends Service {
static {
Service.register(this, {}, {
'devices': ['jsobject'],
@@ -198,5 +198,5 @@ class Bluetooth extends Service {
}
}
const bluetoothService = new Bluetooth();
export default bluetoothService;
export const bluetooth = new Bluetooth;
export default bluetooth;

View File

@@ -5,14 +5,14 @@ import { execAsync } from '../utils.js';
const HIS = GLib.getenv('HYPRLAND_INSTANCE_SIGNATURE');
class Active extends Service {
export class Active extends Service {
updateProperty(prop: string, value: unknown): void {
super.updateProperty(prop, value);
this.emit('changed');
}
}
class ActiveClient extends Active {
export class ActiveClient extends Active {
static {
Service.register(this, {}, {
'address': ['string'],
@@ -30,7 +30,7 @@ class ActiveClient extends Active {
get class() { return this._class; }
}
class ActiveWorkspace extends Active {
export class ActiveWorkspace extends Active {
static {
Service.register(this, {}, {
'id': ['int'],
@@ -45,7 +45,7 @@ class ActiveWorkspace extends Active {
get name() { return this._name; }
}
class Actives extends Service {
export class Actives extends Service {
static {
Service.register(this, {}, {
'client': ['jsobject'],
@@ -76,7 +76,7 @@ class Actives extends Service {
get workspace() { return this._workspace; }
}
class Hyprland extends Service {
export class Hyprland extends Service {
static {
Service.register(this, {
'urgent-window': ['string'],
@@ -312,4 +312,5 @@ class Hyprland extends Service {
}
}
export default new Hyprland();
export const hyprland = new Hyprland;
export default hyprland;

View File

@@ -26,7 +26,7 @@ type MprisMetadata = {
[key: string]: unknown
}
class MprisPlayer extends Service {
export class MprisPlayer extends Service {
static {
Service.register(this, {
'closed': [],
@@ -266,7 +266,7 @@ class MprisPlayer extends Service {
}
type Players = Map<string, MprisPlayer>;
class Mpris extends Service {
export class Mpris extends Service {
static {
Service.register(this, {
'player-changed': ['string'],
@@ -349,4 +349,5 @@ class Mpris extends Service {
}
}
export default new Mpris();
export const mpris = new Mpris;
export default mpris;

View File

@@ -57,7 +57,7 @@ const DEVICE = (device: string) => {
}
};
class Wifi extends Service {
export class Wifi extends Service {
static {
Service.register(this, {}, {
'enabled': ['boolean', 'rw'],
@@ -172,7 +172,7 @@ class Wifi extends Service {
}
}
class Wired extends Service {
export class Wired extends Service {
static {
Service.register(this, {}, {
'speed': ['int'],
@@ -206,14 +206,14 @@ class Wired extends Service {
if (this.internet === 'connected')
return 'network-wired-symbolic';
if (networkService.connectivity !== 'full')
if (network.connectivity !== 'full')
return 'network-wired-no-route-symbolic';
return 'network-wired-disconnected-symbolic';
}
}
class Network extends Service {
export class Network extends Service {
static {
Service.register(this, {}, {
'wifi': ['jsobject'],
@@ -286,5 +286,5 @@ class Network extends Service {
}
}
const networkService = new Network();
export default networkService;
const network = new Network;
export default network;

View File

@@ -46,7 +46,7 @@ const _URGENCY = (urgency?: number) => {
}
};
class Notification extends Service {
export class Notification extends Service {
static {
Service.register(this, {
'dismissed': [],
@@ -196,7 +196,7 @@ class Notification extends Service {
}
}
class Notifications extends Service {
export class Notifications extends Service {
static {
Service.register(this, {
'dismissed': ['int'],
@@ -372,5 +372,5 @@ class Notifications extends Service {
}
}
export default new Notifications();
export const notifications = new Notifications;
export default notifications;

View File

@@ -7,13 +7,18 @@ import DbusmenuGtk3 from 'gi://DbusmenuGtk3';
import Service from '../service.js';
import { StatusNotifierItemProxy } from '../dbus/types.js';
import { bulkConnect, loadInterfaceXML } from '../utils.js';
import Widget from '../widget.js';
import { subclass } from '../widget.js';
const StatusNotifierWatcherIFace = loadInterfaceXML('org.kde.StatusNotifierWatcher')!;
const StatusNotifierItemIFace = loadInterfaceXML('org.kde.StatusNotifierItem')!;
const StatusNotifierItemProxy =
Gio.DBusProxy.makeProxyWrapper(StatusNotifierItemIFace) as unknown as StatusNotifierItemProxy;
const DbusmenuGtk3Menu = subclass<
typeof DbusmenuGtk3.Menu,
DbusmenuGtk3.Menu.ConstructorProperties
>(DbusmenuGtk3.Menu);
export class TrayItem extends Service {
static {
Service.register(this, {
@@ -116,8 +121,7 @@ export class TrayItem extends Service {
private _itemProxyAcquired(proxy: StatusNotifierItemProxy) {
if (proxy.Menu) {
const menu = Widget({
type: DbusmenuGtk3.Menu,
const menu = DbusmenuGtk3Menu({
dbus_name: proxy.g_name_owner,
dbus_object: proxy.Menu,
});
@@ -205,7 +209,7 @@ export class TrayItem extends Service {
}
}
class SystemTray extends Service {
export class SystemTray extends Service {
static {
Service.register(this, {
'added': ['string'],
@@ -286,5 +290,5 @@ class SystemTray extends Service {
}
}
export default new SystemTray();
export const systemTray = new SystemTray;
export default systemTray;

View File

@@ -2,10 +2,6 @@ import Gtk from 'gi://Gtk?version=3.0';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Service from './service.js';
import { App } from './app.js';
import { Variable } from './variable.js';
import { type Command } from './widgets/widget.js';
export const USER = GLib.get_user_name();
export const CACHE_DIR = `${GLib.get_user_cache_dir()}/${pkg.name.split('.').pop()}`;
@@ -25,7 +21,7 @@ export function readFileAsync(path: string): Promise<string> {
const [success, bytes] = file.load_contents_finish(res);
return success
? resolve(new TextDecoder().decode(bytes))
: reject(new Error(
: reject(Error(
`reading file ${path} was unsuccessful`));
} catch (error) {
reject(error);
@@ -91,33 +87,6 @@ export function bulkDisconnect(service: GObject.Object, ids: number[]) {
service.disconnect(id);
}
export function connect(
obj: GObject.Object,
widget: Gtk.Widget,
callback: (widget: Gtk.Widget, ...args: unknown[]) => void,
event?: string,
) {
if (!(obj instanceof Service || obj instanceof App || obj instanceof Variable) && !event)
return console.error(new Error('missing signal for connection'));
const bind = obj.connect(event as string,
(_s, ...args: unknown[]) => callback(widget, ...args));
const w = Object.assign(widget, { _destroyed: false });
w.connect('destroy', () => {
w._destroyed = true;
obj.disconnect(bind);
});
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
if (!w._destroyed)
callback(w);
return GLib.SOURCE_REMOVE;
});
}
export function interval(
interval: number,
callback: () => void,
@@ -141,29 +110,6 @@ export function timeout(ms: number, callback: () => void) {
});
}
export function runCmd(
cmd: Command,
...args: unknown[]
) {
if (typeof cmd !== 'string' && typeof cmd !== 'function') {
console.error('Command has to be string or function');
return false;
}
if (!cmd)
return false;
if (typeof cmd === 'string') {
GLib.spawn_command_line_async(cmd);
return true;
}
if (typeof cmd === 'function')
return cmd(...args);
return false;
}
export function lookUpIcon(name?: string, size = 16) {
if (!name)
return null;
@@ -243,7 +189,7 @@ export function subprocess(
const pipe = proc.get_stdout_pipe();
if (!pipe) {
onError(new Error(`subprocess ${cmd} stdout pipe is null`));
onError(Error(`subprocess ${cmd} stdout pipe is null`));
return null;
}

View File

@@ -4,14 +4,14 @@ import GLib from 'gi://GLib';
import { execAsync, interval, subprocess } from './utils.js';
type Listen<T> =
[string[] | string, (out: string) => T] |
[string[] | string] |
string[] |
string;
| [string[] | string, (out: string) => T]
| [string[] | string]
| string[]
| string;
type Poll<T> =
[number, string[] | string | (() => T)] |
[number, string[] | string | (() => T), (out: string) => T];
| [number, string[] | string | (() => T)]
| [number, string[] | string | (() => T), (out: string) => T];
interface Options<T> {
poll?: Poll<T>

View File

@@ -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<T extends typeof Gtk.Widget>(Widget: T) {
return (...props: ConstructorParameters<T>) => new Widget(...props) as InstanceType<T>;
}
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<
@@ -43,12 +48,16 @@ export function Widget<
>({ type, ...props }:
{ type: T } & Props,
) {
if (ctors.has(type))
return ctors.get(type)(props);
console.warn('Calling Widget({ type }) is deprecated. ' +
`Use Widget.subclass instead, or open up an issue/PR to include ${type.name} on Widget`);
const ctor = AgsWidget(type);
ctors.set(type, ctor);
return ctor(props);
if (ctors.has(type))
// @ts-expect-error
return new ctors.get(type)(props);
const Ctor = AgsWidget(type);
ctors.set(type, Ctor);
return new Ctor(props);
}
// so they are still accessible when importing only Widget
@@ -70,4 +79,57 @@ Widget.Slider = Slider;
Widget.Stack = Stack;
Widget.Window = Window;
export function subclass<T extends typeof Gtk.Widget, Props>(W: T, GTypeName?: string) {
class Widget extends AgsWidget(W, `Gtk${W.name}`) {
static { GObject.registerClass({ GTypeName: GTypeName || `Ags${W.name}` }, this); }
constructor(props: BaseProps<InstanceType<T> & Widget> & Props) {
super(props as Gtk.Widget.ConstructorProperties);
}
}
return (props?: BaseProps<InstanceType<T> & Widget> & Props) => new Widget(props) as InstanceType<T> & Widget;
}
Widget.subclass = subclass;
export const Calendar = subclass<typeof Gtk.Calendar, Gtk.Calendar.ConstructorProperties>(Gtk.Calendar);
Widget.Calendar = Calendar;
export const Fixed = subclass<typeof Gtk.Fixed, Gtk.Fixed.ConstructorProperties>(Gtk.Fixed);
Widget.Fixed = Fixed;
export const MenuBar = subclass<typeof Gtk.MenuBar, Gtk.MenuBar.ConstructorProperties>(Gtk.MenuBar);
Widget.MenuBar = MenuBar;
export const Switch = subclass<typeof Gtk.Switch, Gtk.Switch.ConstructorProperties>(Gtk.Switch);
Widget.Switch = Switch;
export const ToggleButton = subclass<typeof Gtk.ToggleButton, Gtk.ToggleButton.ConstructorProperties>(Gtk.ToggleButton);
Widget.ToggleButton = ToggleButton;
export const Separator = subclass<typeof Gtk.Separator, Gtk.Separator.ConstructorProperties>(Gtk.Separator);
Widget.Separator = Separator;
export const LevelBar = subclass<typeof Gtk.LevelBar, Gtk.LevelBar.ConstructorProperties>(Gtk.LevelBar);
Widget.LevelBar = LevelBar;
export const DrawingArea = subclass<typeof Gtk.DrawingArea, Gtk.DrawingArea.ConstructorProperties>(Gtk.DrawingArea);
Widget.DrawingArea = DrawingArea;
export const FontButton = subclass<typeof Gtk.FontButton, Gtk.FontButton.ConstructorProperties>(Gtk.FontButton);
Widget.FontButton = FontButton;
export const ColorButton = subclass<typeof Gtk.ColorButton, Gtk.ColorButton.ConstructorProperties>(Gtk.ColorButton);
Widget.ColorButton = ColorButton;
export const FileChooserButton = subclass<typeof Gtk.FileChooserButton, Gtk.FileChooserButton.ConstructorProperties>(Gtk.FileChooserButton);
Widget.FileChooserButton = FileChooserButton;
export const SpinButton = subclass<typeof Gtk.SpinButton, Gtk.SpinButton.ConstructorProperties>(Gtk.SpinButton);
Widget.SpinButton = SpinButton;
export const Spinner = subclass<typeof Gtk.Spinner, Gtk.Spinner.ConstructorProperties>(Gtk.Spinner);
Widget.Spinner = Spinner;
export const FlowBox = subclass<typeof Gtk.FlowBox, Gtk.FlowBox.ConstructorProperties>(Gtk.FlowBox);
Widget.FlowBox = FlowBox;
export default Widget;

View File

@@ -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<T> extends BaseProps<T>, 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,12 +19,7 @@ export default class AgsBox extends Gtk.Box {
}, this);
}
constructor({ children, ...rest }: BoxProps = {}) {
super(rest);
if (children)
this.children = children;
}
constructor(props: BoxProps<AgsBox> = {}) { super(props); }
get children() { return this.get_children(); }
set children(children: Gtk.Widget[]) {

View File

@@ -1,111 +1,161 @@
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';
import Service from '../service.js';
export interface ButtonProps extends Gtk.Button.ConstructorProperties {
onClicked?: Command
onPrimaryClick?: Command
onSecondaryClick?: Command
onMiddleClick?: Command
onPrimaryClickRelease?: Command
onSecondaryClickRelease?: Command
onMiddleClickRelease?: Command
onHover?: Command
onHoverLost?: Command
onScrollUp?: Command
onScrollDown?: Command
type EventHandler = (self: AgsButton, event: Gdk.Event) => boolean | unknown;
export interface ButtonProps extends BaseProps<AgsButton>, Gtk.Button.ConstructorProperties {
on_clicked?: (self: AgsButton) => void
on_hover?: EventHandler
on_hover_lost?: EventHandler
on_scroll_up?: EventHandler
on_scroll_down?: EventHandler
on_primary_click?: EventHandler
on_middle_click?: EventHandler
on_secondary_click?: EventHandler
on_primary_click_release?: EventHandler
on_middle_click_release?: EventHandler
on_secondary_click_release?: EventHandler
}
export default class AgsButton extends Gtk.Button {
static { GObject.registerClass(this); }
export default class AgsButton extends AgsWidget(Gtk.Button) {
static {
GObject.registerClass({
GTypeName: 'AgsButton',
Properties: {
'on-clicked': Service.pspec('on-clicked', 'jsobject', 'rw'),
onClicked: Command;
onPrimaryClick: Command;
onSecondaryClick: Command;
onMiddleClick: Command;
onPrimaryClickRelease: Command;
onSecondaryClickRelease: Command;
onMiddleClickRelease: Command;
onHover: Command;
onHoverLost: Command;
onScrollUp: Command;
onScrollDown: Command;
'on-hover':
Service.pspec('on-hover', 'jsobject', 'rw'),
'on-hover-lost':
Service.pspec('on-hover-lost', 'jsobject', 'rw'),
constructor({
onClicked = '',
onPrimaryClick = '',
onSecondaryClick = '',
onMiddleClick = '',
onPrimaryClickRelease = '',
onSecondaryClickRelease = '',
onMiddleClickRelease = '',
onHover = '',
onHoverLost = '',
onScrollUp = '',
onScrollDown = '',
...rest
}: ButtonProps = {}) {
super(rest);
'on-scroll-up':
Service.pspec('on-scroll-up', 'jsobject', 'rw'),
'on-scroll-down':
Service.pspec('on-scroll-down', 'jsobject', 'rw'),
'on-primary-click':
Service.pspec('on-primary-click', 'jsobject', 'rw'),
'on-secondary-click':
Service.pspec('on-secondary-click', 'jsobject', 'rw'),
'on-middle-click':
Service.pspec('on-middle-click', 'jsobject', 'rw'),
'on-primary-click-release':
Service.pspec('on-primary-click-release', 'jsobject', 'rw'),
'on-secondary-click-release':
Service.pspec('on-secondary-click-release', 'jsobject', 'rw'),
'on-middle-click-release':
Service.pspec('on-middle-click-release', 'jsobject', 'rw'),
},
}, this);
}
constructor(props: ButtonProps = {}) {
super(props);
this.add_events(Gdk.EventMask.SCROLL_MASK);
this.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK);
this.onClicked = onClicked;
this.onPrimaryClick = onPrimaryClick;
this.onSecondaryClick = onSecondaryClick;
this.onMiddleClick = onMiddleClick;
this.onPrimaryClickRelease = onPrimaryClickRelease;
this.onSecondaryClickRelease = onSecondaryClickRelease;
this.onMiddleClickRelease = onMiddleClickRelease;
this.onHover = onHover;
this.onHoverLost = onHoverLost;
this.onScrollUp = onScrollUp;
this.onScrollDown = onScrollDown;
this.connect('clicked', () => this.on_clicked?.(this));
this.connect('clicked', (...args) => runCmd(this.onClicked, ...args));
this.connect('enter-notify-event', (btn, event) => {
return runCmd(this.onHover, btn, event);
this.connect('enter-notify-event', (_, event: Gdk.Event) => {
return this.on_hover?.(this, event);
});
this.connect('leave-notify-event', (btn, event) => {
return runCmd(this.onHoverLost, btn, event);
this.connect('leave-notify-event', (_, event: Gdk.Event) => {
return this.on_hover_lost?.(this, event);
});
this.connect('button-press-event', (_, event: Gdk.Event) => {
if (this.onPrimaryClick &&
event.get_button()[1] === Gdk.BUTTON_PRIMARY)
return runCmd(this.onPrimaryClick, this, event);
if (event.get_button()[1] === Gdk.BUTTON_PRIMARY)
return this.on_primary_click?.(this, event);
else if (this.onSecondaryClick &&
event.get_button()[1] === Gdk.BUTTON_SECONDARY)
return runCmd(this.onSecondaryClick, this, event);
else if (event.get_button()[1] === Gdk.BUTTON_MIDDLE)
return this.on_middle_click?.(this, event);
else if (this.onMiddleClick &&
event.get_button()[1] === Gdk.BUTTON_MIDDLE)
return runCmd(this.onMiddleClick, this, event);
else if (event.get_button()[1] === Gdk.BUTTON_SECONDARY)
return this.on_secondary_click?.(this, event);
});
this.connect('button-release-event', (_, event: Gdk.Event) => {
if (this.onPrimaryClickRelease &&
event.get_button()[1] === Gdk.BUTTON_PRIMARY)
return runCmd(this.onPrimaryClickRelease, this, event);
if (event.get_button()[1] === Gdk.BUTTON_PRIMARY)
return this.on_primary_click_release?.(this, event);
else if (this.onSecondaryClickRelease &&
event.get_button()[1] === Gdk.BUTTON_SECONDARY)
return runCmd(this.onSecondaryClickRelease, this, event);
else if (event.get_button()[1] === Gdk.BUTTON_MIDDLE)
return this.on_middle_click_release?.(this, event);
else if (this.onMiddleClickRelease &&
event.get_button()[1] === Gdk.BUTTON_MIDDLE)
return runCmd(this.onMiddleClickRelease, this, event);
else if (event.get_button()[1] === Gdk.BUTTON_SECONDARY)
return this.on_secondary_click_release?.(this, event);
});
this.connect('scroll-event', (box, event) => {
this.connect('scroll-event', (_, event: Gdk.Event) => {
if (event.get_scroll_deltas()[2] < 0)
return runCmd(this.onScrollUp, box, event);
return this.on_scroll_up?.(this, event);
else if (event.get_scroll_deltas()[2] > 0)
return runCmd(this.onScrollDown, box, event);
return this.on_scroll_down?.(this, event);
});
}
get on_clicked() { return this._get('on-clicked'); }
set on_clicked(callback: ButtonProps['on_clicked']) {
this._set('on-clicked', callback);
}
get on_hover() { return this._get('on-hover'); }
set on_hover(callback: EventHandler) {
this._set('on-hover', callback);
}
get on_hover_lost() { return this._get('on-hover-lost'); }
set on_hover_lost(callback: EventHandler) {
this._set('on-hover-lost', callback);
}
get on_scroll_up() { return this._get('on-scroll-up'); }
set on_scroll_up(callback: EventHandler) {
this._set('on-scroll-up', callback);
}
get on_scroll_down() { return this._get('on-scroll-down'); }
set on_scroll_down(callback: EventHandler) {
this._set('on-scroll-down', callback);
}
get on_primary_click() { return this._get('on-primary-click'); }
set on_primary_click(callback: EventHandler) {
this._set('on-primary-click', callback);
}
get on_middle_click() { return this._get('on-middle-click'); }
set on_middle_click(callback: EventHandler) {
this._set('on-middle-click', callback);
}
get on_secondary_click() { return this._get('on-secondary-click'); }
set on_secondary_click(callback: EventHandler) {
this._set('on-secondary-click', callback);
}
get on_primary_click_release() { return this._get('on-primary-click-release'); }
set on_primary_click_release(callback: EventHandler) {
this._set('on-primary-click-release', callback);
}
get on_middle_click_release() { return this._get('on-middle-click-release'); }
set on_middle_click_release(callback: EventHandler) {
this._set('on-middle-click-release', callback);
}
get on_secondary_click_release() { return this._get('on-secondary-click-release'); }
set on_secondary_click_release(callback: EventHandler) {
this._set('on-secondary-click-release', callback);
}
}

View File

@@ -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<AgsCenterBox> {
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',
@@ -32,6 +32,8 @@ export default class AgsCenterBox extends AgsBox {
}, this);
}
constructor(props: CenterBoxProps = {}) { super(props as BoxProps<AgsBox>); }
set children(children: Gtk.Widget[]) {
const newChildren = children || [];
@@ -48,37 +50,31 @@ export default class AgsCenterBox extends AgsBox {
this.end_widget = children[2];
}
// @ts-expect-error
get start_widget() { return this._startWidget || null; }
get start_widget() { return this._get('start-widget') || null; }
set start_widget(child: Gtk.Widget | null) {
if (this.start_widget)
this.start_widget.destroy();
// @ts-expect-error
this._startWidget = child;
this._set('start-widget', child);
if (!child)
return;
this.pack_start(child, true, true, 0);
this.notify('start-widget');
this.show_all();
}
// @ts-expect-error
get end_widget() { return this._endWidget || null; }
get end_widget() { return this._get('end-widget') || null; }
set end_widget(child: Gtk.Widget | null) {
if (this.end_widget)
this.end_widget.destroy();
// @ts-expect-error
this._endWidget = child;
this._set('end-widget', child);
if (!child)
return;
this.pack_end(child, true, true, 0);
this.notify('end-widget');
this.show_all();
}

View File

@@ -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<AgsCircularProgress>, 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,32 +35,27 @@ export default class AgsCircularProgress extends Gtk.Bin {
}, this);
}
// @ts-expect-error
get rounded() { return this._rounded || false; }
constructor(props: CircularProgressProps = {}) { super(props); }
get rounded() { return this._get('rounded') || false; }
set rounded(r: boolean) {
if (this.rounded === r)
return;
// @ts-expect-error
this._rounded = r;
this.notify('rounded');
this._set('rounded', r);
this.queue_draw();
}
// @ts-expect-error
get inverted() { return this._inverted || false; }
get inverted() { return this._get('inverted') || false; }
set inverted(inverted: boolean) {
if (this.inverted === inverted)
return;
// @ts-expect-error
this._inverted = inverted;
this.notify('inverted');
this._set('inverted', inverted);
this.queue_draw();
}
// @ts-expect-error
get start_at() { return this._startAt || 0; }
get start_at() { return this._get('start-at') || 0; }
set start_at(value: number) {
if (this.start_at === value)
return;
@@ -69,14 +66,11 @@ export default class AgsCircularProgress extends Gtk.Bin {
if (value < 0)
value = 0;
// @ts-expect-error
this._startAt = value;
this.notify('start-at');
this._set('start-at', value);
this.queue_draw();
}
// @ts-expect-error
get value() { return this._value || 0; }
get value() { return this._get('value') || 0; }
set value(value: number) {
if (this.value === value)
return;
@@ -88,9 +82,7 @@ export default class AgsCircularProgress extends Gtk.Bin {
value = 0;
// @ts-expect-error
this._value = value;
this.notify('value');
this._set('value', value);
this.queue_draw();
}

View File

@@ -1,35 +1,38 @@
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';
import Service from '../service.js';
export interface EntryProps extends Gtk.Entry.ConstructorProperties {
onAccept?: Command
onChange?: Command
export interface EntryProps extends BaseProps<AgsEntry>, Gtk.Entry.ConstructorProperties {
on_accept?: (self: AgsEntry) => void | unknown
on_change?: (self: AgsEntry) => void | unknown
}
export default class AgsEntry extends Gtk.Entry {
static { GObject.registerClass(this); }
export default class AgsEntry extends AgsWidget(Gtk.Entry) {
static {
GObject.registerClass({
GTypeName: 'AgsEntry',
Properties: {
'on-accept': Service.pspec('on-accept', 'jsobject', 'rw'),
'on-change': Service.pspec('on-change', 'jsobject', 'rw'),
},
}, this);
}
onAccept: Command;
onChange: Command;
constructor(props: EntryProps = {}) {
super(props);
constructor({ onAccept = '', onChange = '', ...rest }: EntryProps = {}) {
super(rest);
this.connect('activate', () => this.on_accept?.(this));
this.connect('notify::text', () => this.on_change?.(this));
}
this.onAccept = onAccept;
this.onChange = onChange;
get on_accept() { return this._get('on-accept'); }
set on_accept(callback: EntryProps['on_accept']) {
this._set('on-accept', callback);
}
this.connect('activate', () => {
typeof this.onAccept === 'function'
? this.onAccept(this)
: runCmd(this.onAccept.replace(/\{\}/g, this.text || ''));
});
this.connect('notify::text', ({ text }, event) => {
typeof this.onChange === 'function'
? this.onChange(this, event)
: runCmd(this.onChange.replace(/\{\}/g, text || ''));
});
get on_change() { return this._get('on-change'); }
set on_change(callback: EntryProps['on_change']) {
this._set('on-change', callback);
}
}

View File

@@ -1,103 +1,154 @@
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';
import Service from '../service.js';
export interface EventBoxProps extends Gtk.EventBox.ConstructorProperties {
onPrimaryClick?: Command
onSecondaryClick?: Command
onMiddleClick?: Command
onPrimaryClickRelease?: Command
onSecondaryClickRelease?: Command
onMiddleClickRelease?: Command
onHover?: Command
onHoverLost?: Command
onScrollUp?: Command
onScrollDown?: Command
type EventHandler = (self: AgsEventBox, event: Gdk.Event) => boolean | unknown;
export interface EventBoxProps extends BaseProps<AgsEventBox>, Gtk.EventBox.ConstructorProperties {
on_hover?: EventHandler
on_hover_lost?: EventHandler
on_scroll_up?: EventHandler
on_scroll_down?: EventHandler
on_primary_click?: EventHandler
on_middle_click?: EventHandler
on_secondary_click?: EventHandler
on_primary_click_release?: EventHandler
on_middle_click_release?: EventHandler
on_secondary_click_release?: EventHandler
}
export default class AgsEventBox extends Gtk.EventBox {
static { GObject.registerClass(this); }
export default class AgsEventBox extends AgsWidget(Gtk.EventBox) {
static {
GObject.registerClass({
GTypeName: 'AgsEventBox',
Properties: {
'on-hover':
Service.pspec('on-hover', 'jsobject', 'rw'),
'on-hover-lost':
Service.pspec('on-hover-lost', 'jsobject', 'rw'),
onPrimaryClick: Command;
onSecondaryClick: Command;
onMiddleClick: Command;
onPrimaryClickRelease: Command;
onSecondaryClickRelease: Command;
onMiddleClickRelease: Command;
onHover: Command;
onHoverLost: Command;
onScrollUp: Command;
onScrollDown: Command;
'on-scroll-up':
Service.pspec('on-scroll-up', 'jsobject', 'rw'),
'on-scroll-down':
Service.pspec('on-scroll-down', 'jsobject', 'rw'),
constructor({
onPrimaryClick = '',
onSecondaryClick = '',
onMiddleClick = '',
onPrimaryClickRelease = '',
onSecondaryClickRelease = '',
onMiddleClickRelease = '',
onHover = '',
onHoverLost = '',
onScrollUp = '',
onScrollDown = '',
...params
}: EventBoxProps = {}) {
super(params);
'on-primary-click':
Service.pspec('on-primary-click', 'jsobject', 'rw'),
'on-secondary-click':
Service.pspec('on-secondary-click', 'jsobject', 'rw'),
'on-middle-click':
Service.pspec('on-middle-click', 'jsobject', 'rw'),
'on-primary-click-release':
Service.pspec('on-primary-click-release', 'jsobject', 'rw'),
'on-secondary-click-release':
Service.pspec('on-secondary-click-release', 'jsobject', 'rw'),
'on-middle-click-release':
Service.pspec('on-middle-click-release', 'jsobject', 'rw'),
},
}, this);
}
constructor(props: EventBoxProps = {}) {
super(props);
this.add_events(Gdk.EventMask.SCROLL_MASK);
this.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK);
this.onPrimaryClick = onPrimaryClick;
this.onSecondaryClick = onSecondaryClick;
this.onMiddleClick = onMiddleClick;
this.onPrimaryClickRelease = onPrimaryClickRelease;
this.onSecondaryClickRelease = onSecondaryClickRelease;
this.onMiddleClickRelease = onMiddleClickRelease;
this.onHover = onHover;
this.onHoverLost = onHoverLost;
this.onScrollUp = onScrollUp;
this.onScrollDown = onScrollDown;
this.connect('enter-notify-event', (box, event) => {
box.set_state_flags(Gtk.StateFlags.PRELIGHT, false);
return runCmd(this.onHover, box, event);
this.connect('enter-notify-event', (_, event: Gdk.Event) => {
this.set_state_flags(Gtk.StateFlags.PRELIGHT, false);
return this.on_hover?.(this, event);
});
this.connect('leave-notify-event', (box, event) => {
box.unset_state_flags(Gtk.StateFlags.PRELIGHT);
return runCmd(this.onHoverLost, box, event);
this.connect('leave-notify-event', (_, event: Gdk.Event) => {
this.unset_state_flags(Gtk.StateFlags.PRELIGHT);
return this.on_hover_lost?.(this, event);
});
this.connect('button-press-event', (box, event) => {
box.set_state_flags(Gtk.StateFlags.ACTIVE, false);
this.connect('button-press-event', (_, event: Gdk.Event) => {
this.set_state_flags(Gtk.StateFlags.ACTIVE, false);
if (event.get_button()[1] === Gdk.BUTTON_PRIMARY)
return runCmd(this.onPrimaryClick, box, event);
else if (event.get_button()[1] === Gdk.BUTTON_SECONDARY)
return runCmd(this.onSecondaryClick, box, event);
return this.on_primary_click?.(this, event);
else if (event.get_button()[1] === Gdk.BUTTON_MIDDLE)
return runCmd(this.onMiddleClick, box, event);
});
this.connect('button-release-event', (box, event) => {
box.unset_state_flags(Gtk.StateFlags.ACTIVE);
if (event.get_button()[1] === Gdk.BUTTON_PRIMARY)
return runCmd(this.onPrimaryClickRelease, box, event);
return this.on_middle_click?.(this, event);
else if (event.get_button()[1] === Gdk.BUTTON_SECONDARY)
return runCmd(this.onSecondaryClickRelease, box, event);
else if (event.get_button()[1] === Gdk.BUTTON_MIDDLE)
return runCmd(this.onMiddleClickRelease, box, event);
return this.on_secondary_click?.(this, event);
});
this.connect('scroll-event', (box, event) => {
this.connect('button-release-event', (_, event: Gdk.Event) => {
this.unset_state_flags(Gtk.StateFlags.ACTIVE);
if (event.get_button()[1] === Gdk.BUTTON_PRIMARY)
return this.on_primary_click_release?.(this, event);
else if (event.get_button()[1] === Gdk.BUTTON_MIDDLE)
return this.on_middle_click_release?.(this, event);
else if (event.get_button()[1] === Gdk.BUTTON_SECONDARY)
return this.on_secondary_click_release?.(this, event);
});
this.connect('scroll-event', (_, event: Gdk.Event) => {
if (event.get_scroll_deltas()[2] < 0)
return runCmd(this.onScrollUp, box, event);
return this.on_scroll_up?.(this, event);
else if (event.get_scroll_deltas()[2] > 0)
return runCmd(this.onScrollDown, box, event);
return this.on_scroll_down?.(this, event);
});
}
get on_hover() { return this._get('on-hover'); }
set on_hover(callback: EventHandler) {
this._set('on-hover', callback);
}
get on_hover_lost() { return this._get('on-hover-lost'); }
set on_hover_lost(callback: EventHandler) {
this._set('on-hover-lost', callback);
}
get on_scroll_up() { return this._get('on-scroll-up'); }
set on_scroll_up(callback: EventHandler) {
this._set('on-scroll-up', callback);
}
get on_scroll_down() { return this._get('on-scroll-down'); }
set on_scroll_down(callback: EventHandler) {
this._set('on-scroll-down', callback);
}
get on_primary_click() { return this._get('on-primary-click'); }
set on_primary_click(callback: EventHandler) {
this._set('on-primary-click', callback);
}
get on_middle_click() { return this._get('on-middle-click'); }
set on_middle_click(callback: EventHandler) {
this._set('on-middle-click', callback);
}
get on_secondary_click() { return this._get('on-secondary-click'); }
set on_secondary_click(callback: EventHandler) {
this._set('on-secondary-click', callback);
}
get on_primary_click_release() { return this._get('on-primary-click-release'); }
set on_primary_click_release(callback: EventHandler) {
this._set('on-primary-click-release', callback);
}
get on_middle_click_release() { return this._get('on-middle-click-release'); }
set on_middle_click_release(callback: EventHandler) {
this._set('on-middle-click-release', callback);
}
get on_secondary_click_release() { return this._get('on-secondary-click-release'); }
set on_secondary_click_release(callback: EventHandler) {
this._set('on-secondary-click-release', callback);
}
}

View File

@@ -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,80 +7,74 @@ 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<AgsIcon>, 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'),
'type': Service.pspec('type', 'string', 'r'),
'size': Service.pspec('size', 'double', 'rw'),
'previous-size': Service.pspec('previous-size', 'double', 'r'),
},
}, this);
}
constructor(params: IconProps | string | GdkPixbuf.Pixbuf = {}) {
const {
icon = '',
size = 0,
...rest
} = params as { icon: string | GdkPixbuf.Pixbuf, size: number };
super(typeof params === 'string' || params instanceof GdkPixbuf.Pixbuf ? {} : rest);
constructor(props: IconProps = {}) {
const { icon = '', ...rest } = props as Props;
super(typeof props === 'string' || props instanceof GdkPixbuf.Pixbuf ? {} : rest);
this.size = size;
this.icon = typeof params === 'string' || params instanceof GdkPixbuf.Pixbuf
? params : icon;
// jsobject pspec can't take a string, so we have to set it after constructor
this.icon = typeof props === 'string' || props instanceof GdkPixbuf.Pixbuf
? props : icon;
}
_size = 0;
_previousSize = 0;
get size() { return this._size || this._previousSize || 13; }
get size() { return this._get('size') || this._get('previous-size') || 0; }
set size(size: number) {
this._size = size;
this.notify('size');
this._set('size', size);
this.queue_draw();
}
// @ts-expect-error
get icon() { return this._icon; }
get icon() { return this._get('icon'); }
set icon(icon: string | GdkPixbuf.Pixbuf) {
if (!icon || this.icon === icon)
return;
this._set('icon', icon);
// @ts-expect-error
this._icon = icon;
this.notify('icon');
if (typeof icon === 'string') {
if (GLib.file_test(icon, GLib.FileTest.EXISTS)) {
// @ts-expect-error
this._type = 'file';
const pb =
GdkPixbuf.Pixbuf.new_from_file_at_size(
this.icon as string,
this.size * this.scale_factor,
this.size * this.scale_factor);
this._set('type', 'file');
if (this.size === 0)
return;
const pb = GdkPixbuf.Pixbuf.new_from_file_at_size(
icon,
this.size * this.scale_factor,
this.size * this.scale_factor,
);
const cs = Gdk.cairo_surface_create_from_pixbuf(pb, 0, this.get_window());
this.set_from_surface(cs);
} else {
// @ts-expect-error
this._type = 'named';
this._set('type', 'named');
this.icon_name = icon;
this.pixel_size = this.size;
}
}
else if (icon instanceof GdkPixbuf.Pixbuf) {
// @ts-expect-error
this._type = 'pixbuf';
const pb_scaled =
icon.scale_simple(
this.size * this.scale_factor,
this.size * this.scale_factor,
GdkPixbuf.InterpType.BILINEAR);
this._set('type', 'pixbuf');
if (this.size === 0)
return;
const pb_scaled = icon.scale_simple(
this.size * this.scale_factor,
this.size * this.scale_factor,
GdkPixbuf.InterpType.BILINEAR,
);
if (pb_scaled) {
const cs = Gdk.cairo_surface_create_from_pixbuf(pb_scaled, 0, this.get_window());
this.set_from_surface(cs);
@@ -91,22 +86,21 @@ export default class AgsIcon extends Gtk.Image {
}
vfunc_draw(cr: cairo.Context): boolean {
if (this._size > 1)
if (this.size > 1)
return super.vfunc_draw(cr);
const size = this.get_style_context()
.get_property('font-size', Gtk.StateFlags.NORMAL) as number;
if (size === this._previousSize)
if (size === this._get('previous-size'))
return super.vfunc_draw(cr);
this._previousSize = size;
this._set('previous-size', size);
// @ts-expect-error
switch (this._type) {
switch (this._get('type')) {
case 'file': {
const pb = GdkPixbuf.Pixbuf.new_from_file_at_size(
this.icon as string,
this._get<string>('icon'),
size * this.scale_factor,
size * this.scale_factor);
@@ -115,15 +109,15 @@ export default class AgsIcon extends Gtk.Image {
break;
}
case 'pixbuf': {
const pb_scaled =
(this.icon as GdkPixbuf.Pixbuf).scale_simple(
size * this.scale_factor,
size * this.scale_factor,
GdkPixbuf.InterpType.BILINEAR);
const pb_scaled = this._get<GdkPixbuf.Pixbuf>('icon').scale_simple(
size * this.scale_factor,
size * this.scale_factor,
GdkPixbuf.InterpType.BILINEAR,
);
if (pb_scaled) {
const cs = Gdk.cairo_surface_create_from_pixbuf(
pb_scaled, 0, this.get_window());
pb_scaled, 0, this.get_window(),
);
this.set_from_surface(cs);
}
break;

View File

@@ -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<AgsLabel>, 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'),
@@ -27,8 +29,8 @@ export default class AgsLabel extends Gtk.Label {
}, this);
}
constructor(params: LabelProps = {}) {
super(typeof params === 'string' ? { label: params } : params);
constructor(props: LabelProps = {}) {
super(typeof props === 'string' ? { label: props } : props);
}
get label() { return super.label || ''; }

View File

@@ -1,39 +1,47 @@
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';
import Service from '../service.js';
export interface MenuProps extends Gtk.Menu.ConstructorProperties {
export interface MenuProps extends BaseProps<AgsMenu>, Gtk.Menu.ConstructorProperties {
children?: Gtk.Widget[]
onPopup?: Command
onMoveScroll?: Command
on_popup?: (
self: AgsMenu,
flipped_rect: any | null,
final_rect: any | null,
flipped_x: boolean,
flipped_y: boolean,
) => void | unknown
on_move_scroll?: (self: AgsMenu, scroll_type: Gtk.ScrollType) => void | unknown
}
export class AgsMenu extends Gtk.Menu {
static { GObject.registerClass(this); }
export class AgsMenu extends AgsWidget(Gtk.Menu) {
static {
GObject.registerClass({
GTypeName: 'AgsMenu',
Properties: {
'children': Service.pspec('children', 'jsobject', 'rw'),
'on-popup': Service.pspec('on-popup', 'jsobject', 'rw'),
'on-move-scroll': Service.pspec('on-move-scroll', 'jsobject', 'rw'),
},
}, this);
}
onPopup: Command;
onMoveScroll: Command;
constructor(props: MenuProps = {}) {
super(props);
constructor({
children,
onPopup = '',
onMoveScroll = '',
...rest
}: MenuProps = {}) {
super(rest);
this.connect('popped-up', (_, ...args) => this.on_popup?.(this, ...args));
this.connect('move-scroll', (_, ...args) => this.on_move_scroll?.(this, ...args));
}
if (children)
this.children = children;
get on_popup() { return this._get('on-popup'); }
set on_popup(callback: MenuProps['on_popup']) {
this._set('on-popup', callback);
}
this.onPopup = onPopup;
this.onMoveScroll = onMoveScroll;
this.connect('popped-up', (...args) =>
runCmd(this.onPopup, ...args));
this.connect('move-scroll', (...args) =>
runCmd(this.onMoveScroll, ...args));
get on_move_scroll() { return this._get('on-move-scroll'); }
set on_move_scroll(callback: MenuProps['on_move_scroll']) {
this._set('on-move-scroll', callback);
}
get children() { return this.get_children(); }
@@ -51,33 +59,45 @@ export class AgsMenu extends Gtk.Menu {
}
}
export interface MenuItemProps extends Gtk.Menu.ConstructorProperties {
onActivate?: Command
onSelect?: Command
onDeselect?: Command
type EventHandler = (self: AgsMenuItem) => boolean | unknown;
export interface MenuItemProps extends BaseProps<AgsMenuItem>, Gtk.MenuItem.ConstructorProperties {
on_activate?: EventHandler
on_select?: EventHandler
on_deselct?: EventHandler
}
export class AgsMenuItem extends Gtk.MenuItem {
static { GObject.registerClass(this); }
export class AgsMenuItem extends AgsWidget(Gtk.MenuItem) {
static {
GObject.registerClass({
GTypeName: 'AgsMenuItem',
Properties: {
'on-activate': Service.pspec('on-activate', 'jsobject', 'rw'),
'on-select': Service.pspec('on-select', 'jsobject', 'rw'),
'on-deselect': Service.pspec('on-deselect', 'jsobject', 'rw'),
},
}, this);
}
onActivate: Command;
onSelect: Command;
onDeselect: Command;
constructor(props: MenuItemProps = {}) {
super(props);
constructor({
onActivate = '',
onSelect = '',
onDeselect = '',
...rest
}: MenuItemProps = {}) {
super(rest);
this.connect('activate', () => this.on_activate?.(this));
this.connect('select', () => this.on_select?.(this));
this.connect('deselect', () => this.on_deselect?.(this));
}
this.onActivate = onActivate;
this.onSelect = onSelect;
this.onDeselect = onDeselect;
get on_activate() { return this._get('on-activate'); }
set on_activate(callback: MenuItemProps['on_activate']) {
this._set('on-activate', callback);
}
this.connect('activate', (...args) => runCmd(this.onActivate, ...args));
this.connect('select', (...args) => runCmd(this.onSelect, ...args));
this.connect('deselect', (...args) => runCmd(this.onDeselect, ...args));
get on_select() { return this._get('on-select'); }
set on_select(callback: EventHandler) {
this._set('on-select', callback);
}
get on_deselect() { return this._get('on-deselect'); }
set on_deselect(callback: EventHandler) {
this._set('on-deselect', callback);
}
}

View File

@@ -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<AgsOverlay>, 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,8 @@ export default class AgsOverlay extends Gtk.Overlay {
}, this);
}
constructor(props: OverlayProps = {}) { super(props); }
get pass_through() {
return this.get_children()
.map(ch => this.get_overlay_pass_through(ch))

View File

@@ -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<AgsProgressBar>, 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,8 @@ export default class AgsProgressBar extends Gtk.ProgressBar {
}, this);
}
constructor(props: ProgressBarProps = {}) { super(props); }
get value() { return this.fraction; }
set value(value: number) {
if (this.value === value)

View File

@@ -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,22 @@ const transitions = [
type Transition = typeof transitions[number];
export interface RevealerProps extends Gtk.Revealer.ConstructorProperties {
transitions?: Transition
export interface RevealerProps extends BaseProps<AgsRevealer>, Gtk.Revealer.ConstructorProperties {
transition?: 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);
}
constructor(props: RevealerProps = {}) { super(props); }
get transition() { return transitions[this.transition_type]; }
set transition(transition: Transition) {
if (!transition || this.transition === transition)
@@ -34,5 +38,6 @@ export default class AgsRevealer extends Gtk.Revealer {
}
this.transition_type = transitions.findIndex(t => t === transition);
this.notify('transition');
}
}

View File

@@ -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<AgsScrollable>, 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'),
@@ -20,16 +23,15 @@ export default class AgsScrollable extends Gtk.ScrolledWindow {
}, this);
}
constructor(params: ScrollableProps = {}) {
constructor(props: ScrollableProps = {}) {
super({
...params,
...props,
hadjustment: new Gtk.Adjustment(),
vadjustment: new Gtk.Adjustment(),
});
}
// @ts-expect-error
get hscroll() { return this._hscroll as Policy; }
get hscroll() { return this._get('hscroll'); }
set hscroll(hscroll: Policy) {
if (!hscroll || this.hscroll === hscroll)
return;
@@ -39,14 +41,11 @@ export default class AgsScrollable extends Gtk.ScrolledWindow {
return;
}
// @ts-expect-error
this._hscroll = hscroll;
this.notify('hscroll');
this.policy();
this._set('hscroll', hscroll);
this._policy();
}
// @ts-expect-error
get vscroll() { return this._vscroll as Policy; }
get vscroll() { return this._get('vscroll'); }
set vscroll(vscroll: Policy) {
if (!vscroll || this.vscroll === vscroll)
return;
@@ -56,13 +55,11 @@ export default class AgsScrollable extends Gtk.ScrolledWindow {
return;
}
// @ts-expect-error
this._vscroll = vscroll;
this.notify('vscroll');
this.policy();
this._set('vscroll', vscroll);
this._policy();
}
policy() {
private _policy() {
const hscroll = policy.findIndex(p => p === this.hscroll);
const vscroll = policy.findIndex(p => p === this.vscroll);
this.set_policy(

View File

@@ -1,21 +1,23 @@
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';
import Service from '../service.js';
export interface SliderProps extends Gtk.Scale.ConstructorProperties {
onChange?: Command
type EventHandler = (self: AgsSlider, event: Gdk.Event) => void | unknown;
export interface SliderProps extends BaseProps<AgsSlider>, Gtk.Scale.ConstructorProperties {
on_change?: EventHandler,
value?: number
min?: number
max?: number
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'),
@@ -23,14 +25,12 @@ export default class AgsSlider extends Gtk.Scale {
'min': Service.pspec('min', 'double', 'rw'),
'max': Service.pspec('max', 'double', 'rw'),
'step': Service.pspec('step', 'double', 'rw'),
'on-change': Service.pspec('on-change', 'jsobject', 'rw'),
},
}, this);
}
onChange: Command;
constructor({
onChange = '',
value = 0,
min = 0,
max = 1,
@@ -43,24 +43,21 @@ export default class AgsSlider extends Gtk.Scale {
lower: min,
upper: max,
step_increment: step,
value: value,
}),
});
this.onChange = onChange;
this.adjustment.connect('notify::value', ({ value }, event) => {
this.adjustment.connect('notify::value', (_, event: Gdk.Event) => {
if (!this.dragging)
return;
typeof this.onChange === 'function'
? this.onChange(this, event, value)
: runCmd((onChange as string).replace(/\{\}/g, `${value}`));
this.on_change?.(this, event);
});
if (value)
this.value = value;
}
get on_change() { return this._get('on-change'); }
set on_change(callback: EventHandler) { this._set('on-change', callback); }
get value() { return this.adjustment.value; }
set value(value: number) {
if (this.dragging || this.value === value)
@@ -97,16 +94,8 @@ export default class AgsSlider extends Gtk.Scale {
this.notify('step');
}
// @ts-expect-error
get dragging() { return this._dragging; }
set dragging(dragging: boolean) {
if (this.dragging === dragging)
return;
// @ts-expect-error
this._dragging = dragging;
this.notify('dragging');
}
get dragging() { return this._get('dragging'); }
set dragging(dragging: boolean) { this._set('dragging', dragging); }
get vertical() { return this.orientation === Gtk.Orientation.VERTICAL; }
set vertical(vertical) {

View File

@@ -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<AgsStack>, 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,19 +32,19 @@ 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);
this.notify('items');
}
get items() {
// @ts-expect-error
if (!Array.isArray(this._items))
// @ts-expect-error
this._items = [];
if (!Array.isArray(this._get('items')))
this._set('items', []);
// @ts-expect-error
return this._items;
return this._get('items');
}
set items(items: [string, Gtk.Widget][]) {
@@ -57,13 +59,11 @@ export default class AgsStack extends Gtk.Stack {
.filter(([, ch]) => this.get_children().includes(ch))
.forEach(([, ch]) => this.remove(ch));
// @ts-expect-error
this._items = [];
items.forEach(([name, widget]) => {
widget && this.add_named(widget, name);
widget && super.add_named(widget, name);
});
this.notify('items');
this._set('items', items);
this.show_all();
}

View File

@@ -1,83 +1,90 @@
import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk?version=3.0';
import GLib from 'gi://GLib?version=2.0';
import Service from '../service.js';
import { interval, connect } from '../utils.js';
import { interval } from '../utils.js';
import { Variable } from '../variable.js';
import { App } from '../app.js';
// TODO: remove this type and make them only functions
export type Command = string | ((...args: unknown[]) => boolean);
type KebabCase<S extends string> = S extends `${infer Prefix}_${infer Suffix}`
? `${Prefix}-${KebabCase<Suffix>}` : S;
type OnlyString<S extends string | unknown> = S extends string ? S : never;
const aligns = ['fill', 'start', 'end', 'center', 'baseline'] as const;
type Align = typeof aligns[number];
export interface BaseProps<Self> {
className?: string
classNames?: string[]
style?: string
type Property = [prop: string, value: unknown];
type Connection<Self> =
| [GObject.Object, (self: Self, ...args: unknown[]) => unknown, string?]
| [string, (self: Self, ...args: unknown[]) => unknown]
| [number, (self: Self, ...args: unknown[]) => unknown];
type Bind = [
prop: string,
obj: GObject.Object,
objProp?: string,
transform?: (value: unknown) => unknown,
];
export interface BaseProps<Self> extends Gtk.Widget.ConstructorProperties {
class_name?: string
class_names?: 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<Self>[]
properties?: Property[]
binds?: Bind[],
setup?: (self: Self) => void
}
export default function <T extends typeof Gtk.Widget>(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 <T extends WidgetCtor>(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
'properties': pspec('properties'),
'setup': pspec('setup'),
'connections': pspec('connections'),
'binds': pspec('binds'),
},
}, this);
}
constructor(params: BaseProps<InstanceType<AgsWidget & T>> & ConstructorParameters<T>[0]) {
const {
connections = [],
properties = [],
binds = [],
style,
halign,
valign,
setup,
...rest
} = params;
super(typeof params === 'string' ? params : rest as Gtk.Widget.ConstructorProperties);
_destroyed = false;
this.style = style!;
this.halign = halign!;
this.valign = valign!;
// defining private fields for typescript causes
// gobject constructor field setters to be overridden
// so we use this _get and _set to avoid @ts-expect-error everywhere
_get<T>(field: string) {
return (this as unknown as { [key: string]: unknown })[`_${field}`] as T;
}
const widget = this as InstanceType<AgsWidget & T>;
_set<T>(field: string, value: T) {
if (this._get(field) === value)
return;
properties.forEach(([key, value]) => {
(this as unknown as { [key: string]: unknown })[`_${key}`] = value;
});
(this as unknown as { [key: string]: T })[`_${field}`] = value;
this.notify(field);
}
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<AgsWidget>[]) {
if (!connections)
return;
connections.forEach(([s, callback, event]) => {
if (!s || !callback) {
@@ -89,49 +96,116 @@ export default function <T extends typeof Gtk.Widget>(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);
this.connectTo(s, callback, event);
else
console.error(Error(`${s} is not a GObject nor a string nor a number`));
});
if (typeof setup === 'function')
setup(widget);
}
// @ts-expect-error prop override
get halign() { return aligns[super.halign]; }
set binds(binds: Bind[]) {
if (!binds)
return;
// @ts-expect-error prop override
set halign(align: Align) {
binds.forEach(([prop, obj, objProp = 'value', transform = out => out]) => {
this.bind(
prop as KebabCase<OnlyString<keyof this>>,
obj,
objProp as keyof typeof obj,
transform,
);
});
}
set properties(properties: Property[]) {
if (!properties)
return;
properties.forEach(([key, value]) => {
(this as unknown as { [key: string]: unknown })[`_${key}`] = value;
});
}
set setup(setup: (self: AgsWidget) => void) {
if (!setup)
return;
setup(this);
}
connectTo<GObject extends GObject.Object>(
o: GObject | Service,
callback: (self: typeof this, ...args: unknown[]) => void,
event?: string,
) {
if (!(o instanceof GObject.Object)) {
console.error(Error(`${o} is not a GObject`));
return this;
}
if (!(o instanceof Service || o instanceof App || o instanceof Variable) && !event) {
console.error(Error('you are trying to connect to a regular GObject ' +
'without specifying the signal'));
return this;
}
const id = o.connect(event!, (_, ...args: unknown[]) => callback(this, ...args));
this.connect('destroy', () => {
this._destroyed = true;
o.disconnect(id);
});
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
if (!this._destroyed)
callback(this);
return GLib.SOURCE_REMOVE;
});
return this;
}
bind<GObject extends GObject.Object>(
prop: KebabCase<OnlyString<keyof typeof this>>,
target: GObject,
targetProp: OnlyString<keyof GObject>,
// FIXME: typeof target[targetProp]
transform: (value: typeof target[typeof targetProp]) => unknown = out => out,
) {
// @ts-expect-error readonly property
const callback = () => this[prop] = transform(target[targetProp]);
this.connectTo(target, callback, `notify::${targetProp}`);
return this;
}
get hpack() { return aligns[this.halign]; }
set hpack(align: Align) {
if (!align)
return;
if (!aligns.includes(align)) {
console.error(new Error(`halign has to be on of ${aligns}`));
console.error(Error(`halign has to be on of ${aligns}`));
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;
if (!aligns.includes(align)) {
console.error(new Error(`valign has to be on of ${aligns}`));
console.error(Error(`valign has to be on of ${aligns}`));
return;
}
super.valign = aligns.findIndex(a => a === align);
this.valign = aligns.findIndex(a => a === align);
}
toggleClassName(className: string, condition = true) {
@@ -161,8 +235,11 @@ export default function <T extends typeof Gtk.Widget>(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 +251,15 @@ export default function <T extends typeof Gtk.Widget>(Widget: T) {
this.notify('css');
}
setStyle(css: string) {
this.setCss(`* { ${css} }`);
this.notify('style');
get css() {
return this._cssProvider.to_string() || '';
}
// @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');
}
// @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 {
@@ -220,20 +277,7 @@ export default function <T extends typeof Gtk.Widget>(Widget: T) {
// @ts-expect-error
this.set_child(child);
else
console.error(new Error(`can't set child on ${this}`));
console.error(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<InstanceType<AgsWidget & T>> &
ConstructorParameters<T>[0] |
string,
) => {
return new AgsWidget(params) as InstanceType<AgsWidget & T>;
};
}

View File

@@ -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<Gtk.Window.ConstructorProperties, 'margin'> {
export interface WindowProps extends BaseProps<AgsWindow>, 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,25 +60,22 @@ 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;
this.visible = visible === true || visible === null && !popup;
}
// @ts-expect-error
get monitor() { return this._monitor; }
get monitor(): Gdk.Monitor { return this._get('monitor'); }
set monitor(monitor: number) {
if (monitor < 0 || this.monitor === monitor)
if (monitor < 0)
return;
const m = Gdk.Display.get_default()?.get_monitor(monitor);
if (m) {
LayerShell.set_monitor(this, m);
// @ts-expect-error
this._monitor = monitor;
this.notify('monitor');
this._set('monitor', monitor);
return;
}
@@ -130,15 +129,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,33 +159,26 @@ export default class AgsWindow extends Gtk.Window {
LayerShell.Edge[side], (margin as number[])[i]),
);
this.notify('margin');
this.notify('margins');
}
// @ts-expect-error
get popup() { return !!this._popup; }
// this will be removed in gtk4
get popup() { return !!this._get('popup'); }
set popup(popup: boolean) {
if (this.popup === popup)
return;
// @ts-expect-error
if (this._popup)
// @ts-expect-error
this.disconnect(this._popup);
if (this.popup)
this.disconnect(this._get('popup'));
if (popup) {
this.connect('key-press-event', (_, event) => {
this._set('popup', this.connect('key-press-event', (_, event: Gdk.Event) => {
if (event.get_keyval()[1] === Gdk.KEY_Escape) {
App.getWindow(this.name!)
? App.closeWindow(this.name!)
: this.hide();
}
});
}));
}
this.notify('popup');
}
get focusable() {

View File

@@ -2,7 +2,9 @@
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2017"],
"lib": [
"ES2022"
],
"allowJs": true,
"checkJs": false,
"outDir": "_build/tsc-out",

2
types/ambient.d.ts vendored
View File

@@ -16,6 +16,8 @@ declare module console {
export function error(msg: string, subsitutions?: any[]): void;
export function log(obj: object, others?: object[]): void;
export function log(msg: string, subsitutions?: any[]): void;
export function warn(obj: object, others?: object[]): void;
export function warn(msg: string, subsitutions?: any[]): void;
}
declare interface String {