mirror of
https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio.git
synced 2024-06-10 11:49:22 +00:00
Compare commits
52 commits
Author | SHA1 | Date | |
---|---|---|---|
c309e94db2 | |||
c60fbf8342 | |||
671fd8ffd2 | |||
b8a92586d4 | |||
1f200f4f30 | |||
44d64ccdc4 | |||
3985458832 | |||
7a70feedba | |||
88afa4a99e | |||
d32c75b639 | |||
7789588aef | |||
b1ad0e958b | |||
9c8a578e05 | |||
3e15b7cecb | |||
11cf962bfd | |||
a25f0499c8 | |||
51769d6061 | |||
af317eee96 | |||
3aded523c2 | |||
d3005335b8 | |||
18458e3465 | |||
a6f03db8f6 | |||
bed8d6a58e | |||
c5f9cac444 | |||
30baa56881 | |||
13165fa9c0 | |||
6ca3059914 | |||
f4019fd2af | |||
8c6cda2e92 | |||
9c03de5d00 | |||
24121856ee | |||
e410289a13 | |||
2601454143 | |||
f47b0624fd | |||
b2c6a8bc2a | |||
d002e2811f | |||
0148a43946 | |||
e12fecf971 | |||
886c099dba | |||
f81bedb71a | |||
2787211f0d | |||
98d6451e74 | |||
970103ddee | |||
aa551db066 | |||
4569fa79e8 | |||
be2d7ab6d0 | |||
cf438b523c | |||
66cd1b9c15 | |||
1789bb0b25 | |||
c1ae2c37b6 | |||
a32b16c467 | |||
06b0ec8ead |
|
@ -9,6 +9,7 @@ stages:
|
|||
- lint
|
||||
- test
|
||||
- release
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
FDO_UPSTREAM_REPO: "dabrain34/GstPipelineStudio"
|
||||
|
@ -21,7 +22,7 @@ variables:
|
|||
variables:
|
||||
FDO_DISTRIBUTION_VERSION: "38"
|
||||
# Update this to trigger a container rebuild
|
||||
FDO_DISTRIBUTION_TAG: "2023-08-25.1"
|
||||
FDO_DISTRIBUTION_TAG: $GST_RS_FDO_IMG_TAG
|
||||
before_script:
|
||||
- source ./ci/env.sh
|
||||
- mkdir .cargo && echo -e "[net]\ngit-fetch-with-cli = true" > .cargo/config
|
||||
|
@ -59,7 +60,8 @@ build-fedora-container:
|
|||
bison
|
||||
FDO_DISTRIBUTION_EXEC: >-
|
||||
ci/install-rust.sh stable &&
|
||||
pip3 install meson
|
||||
pip3 install meson &&
|
||||
pip3 install pre-commit
|
||||
|
||||
.windows rust docker build:
|
||||
stage: prepare
|
||||
|
@ -89,10 +91,9 @@ build-fedora-container:
|
|||
windows rust docker stable:
|
||||
extends: ".windows rust docker build"
|
||||
variables:
|
||||
RUST_IMAGE: !reference [variables, "WINDOWS_RUST_STABLE_IMAGE"]
|
||||
RUST_UPSTREAM_IMAGE:
|
||||
!reference [variables, "WINDOWS_RUST_STABLE_UPSTREAM_IMAGE"]
|
||||
RUST_VERSION: !reference [variables, "GST_RS_STABLE"]
|
||||
RUST_IMAGE: $WINDOWS_RUST_STABLE_IMAGE
|
||||
RUST_UPSTREAM_IMAGE: $WINDOWS_RUST_STABLE_UPSTREAM_IMAGE"]
|
||||
RUST_VERSION: $GST_RS_STABLE
|
||||
|
||||
.msvc2019 build:
|
||||
stage: test
|
||||
|
@ -119,15 +120,27 @@ windows installer stable:
|
|||
expire_in: 10 days
|
||||
when: "manual"
|
||||
|
||||
rustfmt-clippy:
|
||||
pre-commit:
|
||||
stage: "lint"
|
||||
extends:
|
||||
- .fedora
|
||||
- .fdo.distribution-image@fedora
|
||||
variables:
|
||||
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||
cache:
|
||||
paths:
|
||||
- ${PRE_COMMIT_HOME}
|
||||
script:
|
||||
- meson setup build
|
||||
- pre-commit run --all-files
|
||||
|
||||
clippy:
|
||||
extends:
|
||||
- .fedora
|
||||
- .fdo.distribution-image@fedora
|
||||
stage: lint
|
||||
script:
|
||||
- meson build
|
||||
- cargo fmt --version
|
||||
- cargo fmt -- --color=always --check
|
||||
- cargo clippy --version
|
||||
- cargo clippy --color=always --all-targets -- -D warnings
|
||||
|
||||
|
@ -227,7 +240,7 @@ flatpak:
|
|||
macos installer stable:
|
||||
stage: test
|
||||
tags:
|
||||
- gst-macos-11.1
|
||||
- gst-macos-13
|
||||
before_script:
|
||||
- pip3 install --upgrade pip
|
||||
# Make sure meson is up to date
|
||||
|
@ -265,3 +278,19 @@ macos installer release:
|
|||
- installer/**/GstPipelineStudio*.dmg
|
||||
- installer/**/GstPipelineStudio*.tar.gz
|
||||
when: "always"
|
||||
|
||||
pages:
|
||||
image: ruby:2.7
|
||||
stage: deploy
|
||||
script:
|
||||
- gem install bundler -v 2.4.22
|
||||
- bundle install
|
||||
- bundle exec jekyll build -d public
|
||||
artifacts:
|
||||
paths:
|
||||
# The folder that contains the files to be exposed at the Page URL
|
||||
- public
|
||||
rules:
|
||||
# This ensures that only pushes to the default branch will trigger
|
||||
# a pages deploy
|
||||
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
|
||||
|
|
29
.pre-commit-config.yaml
Normal file
29
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,29 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.1.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.17.0
|
||||
hooks:
|
||||
- id: typos
|
||||
exclude: '^$|\.svg$'
|
||||
|
||||
- repo: https://github.com/doublify/pre-commit-rust
|
||||
rev: v1.0
|
||||
hooks:
|
||||
- id: fmt
|
1013
Cargo.lock
generated
1013
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
@ -1,15 +1,15 @@
|
|||
[package]
|
||||
name = "gst_pipeline_studio"
|
||||
version = "0.3.2"
|
||||
name = "gst-pipeline-studio"
|
||||
version = "0.3.5"
|
||||
edition = "2018"
|
||||
rust-version = "1.70.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
gtk = { version = "0.7.2", package = "gtk4" }
|
||||
gst = { package = "gstreamer", version = "0.21.0" }
|
||||
gst-plugin-gtk4 = { version = "0.11.0", optional=true }
|
||||
gtk = { version = "0.8.2", package = "gtk4" }
|
||||
gst = { package = "gstreamer", version = "0.22.2" }
|
||||
gst-plugin-gtk4 = { version = "0.12.1", optional=true }
|
||||
anyhow = "1"
|
||||
log = "0.4.11"
|
||||
once_cell = "1.7.2"
|
||||
|
@ -18,6 +18,11 @@ serde = "1.0"
|
|||
serde_any = "0.5"
|
||||
simplelog = "0.11.2"
|
||||
futures-channel = "0.3"
|
||||
lazy_static = "1.4"
|
||||
chrono = "0.4"
|
||||
structopt = "0.3"
|
||||
async-channel = "2.0.0"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
futures-executor = "0.3"
|
||||
|
|
23
ChangeLog.md
23
ChangeLog.md
|
@ -103,4 +103,25 @@
|
|||
|
||||
## 0.3.2
|
||||
### app
|
||||
- [x] check that element exists before creating it on file load.
|
||||
- [x] check that element exists before creating it on file load.
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### app
|
||||
- [x] Fix MacOs GTK runtime dependencies
|
||||
- [x] Fix the maximize call with MacOS
|
||||
- [x] Fix the default size at GTK save/load state
|
||||
|
||||
## 0.3.4
|
||||
|
||||
### app
|
||||
- [x] Fix first run when application folder has not been created, fixes #23
|
||||
- [x] Fix windows installer to bring share folder and let filesrc work properly, fixes #24
|
||||
|
||||
## 0.3.5
|
||||
|
||||
### app
|
||||
- [x] logs: receive multiple log sources such as GST logs and messages.
|
||||
- [x] settings: add a log level selection
|
||||
- [x] rename gst_pipeline_studio to gst-pipeline-studio
|
||||
- [x] can open a pipeline from the command line
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# Important
|
||||
|
||||
# [GstPipelineStudio](https://dabrain34.pages.freedesktop.org/GstPipelineStudio): Draw your own GStreamer pipeline ...
|
||||
|
||||
## Important
|
||||
|
||||
Until version 1.0, this software should be considered as **unstable**.
|
||||
The settings moreover the graph file format might change over the development phase.
|
||||
|
||||
# GstPipelineStudio: Draw your own GStreamer pipeline ...
|
||||
|
||||
## Description
|
||||
|
||||
|
@ -49,7 +51,7 @@ brew install gstreamer gst-plugins-base gst-plugins-bad
|
|||
```sh
|
||||
$ meson builddir -Dbuildtype=release
|
||||
$ ninja -C builddir
|
||||
$ ./builddir/target/release/gst_pipeline_studio
|
||||
$ ./builddir/target/release/gst-pipeline-studio
|
||||
```
|
||||
|
||||
## Flatpak
|
||||
|
|
2
TODO.md
2
TODO.md
|
@ -14,14 +14,12 @@
|
|||
- [ ] Control the connection between element
|
||||
- [ ] unable to connect element with incompatible caps.
|
||||
- [ ] Implement graph dot render/load
|
||||
|
||||
- [ ] Add probes on each pad to monitor the pipeline
|
||||
- [ ] Render a media file
|
||||
- [ ] Offer compatible element to a pad (autorender)
|
||||
- [ ] Display tags/meta/message detected
|
||||
- [ ] Change TreeView to ListView
|
||||
- [ ] Implement zoom on the view (https://gitlab.gnome.org/World/obfuscate/-/blob/master/src/widgets/drawing_area.rs)
|
||||
- [ ] Settings: add a log level selection
|
||||
- [ ] reopen the last log on prematured exit (crash)
|
||||
- [ ] Play/pause should be prevented until the pipeline is ready
|
||||
- [ ] Filter the elements by class/rank etc.
|
||||
|
|
|
@ -7,26 +7,39 @@ import shutil
|
|||
|
||||
env = os.environ
|
||||
|
||||
MESON_BUILD_ROOT=sys.argv[1]
|
||||
MESON_SOURCE_ROOT=sys.argv[2]
|
||||
CARGO_TARGET_DIR = os.path.join (MESON_BUILD_ROOT, "target")
|
||||
MESON_BUILD_ROOT = sys.argv[1]
|
||||
MESON_SOURCE_ROOT = sys.argv[2]
|
||||
CARGO_TARGET_DIR = os.path.join(MESON_BUILD_ROOT, "target")
|
||||
env["CARGO_TARGET_DIR"] = CARGO_TARGET_DIR
|
||||
env["CARGO_HOME"] = os.path.join (CARGO_TARGET_DIR, "cargo-home")
|
||||
OUTPUT=sys.argv[3]
|
||||
BUILDTYPE=sys.argv[4]
|
||||
APP_BIN=sys.argv[5]
|
||||
env["PKG_CONFIG_PATH"] = os.path.join(MESON_BUILD_ROOT, "meson-uninstalled") + os.pathsep + env.get("PKG_CONFIG_PATH",'')
|
||||
env["CARGO_HOME"] = os.path.join(CARGO_TARGET_DIR, "cargo-home")
|
||||
OUTPUT = sys.argv[3]
|
||||
BUILDTYPE = sys.argv[4]
|
||||
APP_BIN = sys.argv[5]
|
||||
env["PKG_CONFIG_PATH"] = (
|
||||
os.path.join(MESON_BUILD_ROOT, "meson-uninstalled")
|
||||
+ os.pathsep
|
||||
+ env.get("PKG_CONFIG_PATH", "")
|
||||
)
|
||||
|
||||
|
||||
if BUILDTYPE == "release":
|
||||
if BUILDTYPE == "release":
|
||||
print("RELEASE MODE")
|
||||
CMD = ['cargo', 'build', '--manifest-path', os.path.join(MESON_SOURCE_ROOT, 'Cargo.toml'), '--release']
|
||||
CMD = [
|
||||
"cargo",
|
||||
"build",
|
||||
"--manifest-path",
|
||||
os.path.join(MESON_SOURCE_ROOT, "Cargo.toml"),
|
||||
"--release",
|
||||
]
|
||||
subprocess.run(CMD, env=env)
|
||||
shutil.copy2(os.path.join(CARGO_TARGET_DIR, "release", APP_BIN), OUTPUT)
|
||||
else:
|
||||
print("DEBUG MODE")
|
||||
CMD = ['cargo', 'build', '--manifest-path', os.path.join(MESON_SOURCE_ROOT, 'Cargo.toml')]
|
||||
CMD = [
|
||||
"cargo",
|
||||
"build",
|
||||
"--manifest-path",
|
||||
os.path.join(MESON_SOURCE_ROOT, "Cargo.toml"),
|
||||
]
|
||||
subprocess.run(CMD, env=env)
|
||||
shutil.copy2(os.path.join(CARGO_TARGET_DIR, "debug", APP_BIN), OUTPUT)
|
||||
|
||||
|
||||
|
|
|
@ -3,19 +3,17 @@
|
|||
from os import environ, path
|
||||
from subprocess import call
|
||||
|
||||
prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
|
||||
datadir = path.join(prefix, 'share')
|
||||
destdir = environ.get('DESTDIR', '')
|
||||
prefix = environ.get("MESON_INSTALL_PREFIX", "/usr/local")
|
||||
datadir = path.join(prefix, "share")
|
||||
destdir = environ.get("DESTDIR", "")
|
||||
|
||||
# Package managers set this so we don't need to run
|
||||
if not destdir:
|
||||
print('Updating icon cache...')
|
||||
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
|
||||
|
||||
print('Updating desktop database...')
|
||||
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
|
||||
|
||||
print('Compiling GSettings schemas...')
|
||||
call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])
|
||||
print("Updating icon cache...")
|
||||
call(["gtk-update-icon-cache", "-qtf", path.join(datadir, "icons", "hicolor")])
|
||||
|
||||
print("Updating desktop database...")
|
||||
call(["update-desktop-database", "-q", path.join(datadir, "applications")])
|
||||
|
||||
print("Compiling GSettings schemas...")
|
||||
call(["glib-compile-schemas", path.join(datadir, "glib-2.0", "schemas")])
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"sdk-extensions": [
|
||||
"org.freedesktop.Sdk.Extension.rust-stable"
|
||||
],
|
||||
"command": "gst_pipeline_studio",
|
||||
"command": "gst-pipeline-studio",
|
||||
"finish-args": [
|
||||
"--socket=fallback-x11",
|
||||
"--socket=wayland",
|
||||
|
@ -45,7 +45,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"name": "gst_pipeline_studio",
|
||||
"name": "gst-pipeline-studio",
|
||||
"buildsystem": "meson",
|
||||
"run-tests": true,
|
||||
"config-opts": [
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
$env:MESON_ARGS = "--prefix=C:\gst-install\"
|
||||
$env:MESON_ARGS = "--prefix=C:\gst-install\ -Dbuildtype=release"
|
||||
cmd.exe /C "C:\BuildTools\Common7\Tools\VsDevCmd.bat -host_arch=amd64 -arch=amd64 && meson _build $env:MESON_ARGS && meson compile -C _build && ninja -C _build install"
|
||||
if (!$?) {
|
||||
Write-Host "Failed to build and install GstPipelineStudio"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
variables:
|
||||
GST_RS_WIN_IMG_TAG: "2023-08-31.0"
|
||||
GST_RS_FDO_IMG_TAG: "2023-08-25.1"
|
||||
GST_RS_WIN_IMG_TAG: "2023-12-22.0"
|
||||
GST_RS_FDO_IMG_TAG: "2024-01-05.0"
|
||||
GST_RS_STABLE: "1.70.0"
|
||||
|
|
|
@ -26,7 +26,7 @@ if (!$?) {
|
|||
Exit 1
|
||||
}
|
||||
|
||||
$env:MESON_ARGS = "--prefix=C:\gst-install\ " +
|
||||
$env:MESON_ARGS = "--prefix=C:\gst-install\ -Dbuildtype=release " +
|
||||
"-Dglib:installed_tests=false " +
|
||||
"-Dlibnice:tests=disabled " +
|
||||
"-Dlibnice:examples=disabled " +
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
|
||||
|
||||
$env:MESON_ARGS = "--prefix=C:\gst-install\"
|
||||
$env:MESON_ARGS = "--prefix=C:\gst-install\ -Dbuildtype=release"
|
||||
|
||||
# Download gtk and all its subprojects
|
||||
git clone -b $env:DEFAULT_GTK_BRANCH --depth 1 https://gitlab.gnome.org/gnome/gtk.git C:\gtk
|
||||
|
|
573
data/css/style.css
Normal file
573
data/css/style.css
Normal file
|
@ -0,0 +1,573 @@
|
|||
/*
|
||||
|
||||
GstPipelineStudio Website
|
||||
====================
|
||||
|
||||
shamelessly stolen CSS from Pipewire
|
||||
|
||||
*/
|
||||
|
||||
/* GNOME Color Palette */
|
||||
:root {
|
||||
--rounded-corner: 12px;
|
||||
--blue1: rgb(153, 193, 241);
|
||||
--blue2: rgb(98, 160, 234);
|
||||
--blue3: rgb(53, 132, 228);
|
||||
--blue4: rgb(28, 113, 216);
|
||||
--blue5: rgb(26, 95, 180);
|
||||
--green1: rgb(143, 240, 164);
|
||||
--green2: rgb(87, 227, 137);
|
||||
--green3: rgb(51, 209, 122);
|
||||
--green4: rgb(46, 194, 126);
|
||||
--green5: rgb(38, 162, 105);
|
||||
--yellow1: rgb(249, 240, 107);
|
||||
--yellow2: rgb(248, 228, 92);
|
||||
--yellow3: rgb(246, 211, 45);
|
||||
--yellow4: rgb(245, 194, 17);
|
||||
--yellow5: rgb(229, 165, 10);
|
||||
--orange1: rgb(255, 190, 111);
|
||||
--orange2: rgb(255, 163, 72);
|
||||
--orange3: rgb(255, 120, 0);
|
||||
--orange4: rgb(230, 97, 0);
|
||||
--orange5: rgb(198, 70, 0);
|
||||
--red1: rgb(246, 97, 81);
|
||||
--red2: rgb(237, 51, 59);
|
||||
--red3: rgb(224, 27, 36);
|
||||
--red4: rgb(192, 28, 40);
|
||||
--red5: rgb(165, 29, 45);
|
||||
--purple1: rgb(220, 138, 221);
|
||||
--purple2: rgb(192, 97, 203);
|
||||
--purple3: rgb(145, 65, 172);
|
||||
--purple4: rgb(129, 61, 156);
|
||||
--purple5: rgb(97, 53, 131);
|
||||
--brown1: rgb(205, 171, 143);
|
||||
--brown2: rgb(181, 131, 90);
|
||||
--brown3: rgb(152, 106, 68);
|
||||
--brown4: rgb(134, 94, 60);
|
||||
--brown5: rgb(99, 69, 44);
|
||||
--light1: rgb(255, 255, 255);
|
||||
--light2: rgb(246, 245, 244);
|
||||
--light3: rgb(222, 221, 218);
|
||||
--light4: rgb(192, 191, 188);
|
||||
--light5: rgb(154, 153, 150);
|
||||
--dark1: rgb(119, 118, 123);
|
||||
--dark2: rgb(94, 92, 100);
|
||||
--dark3: rgb(61, 56, 70);
|
||||
--dark4: rgb(36, 31, 49);
|
||||
--dark5: rgb(0, 0, 0);
|
||||
--primary-color: var(--blue5);
|
||||
/* Set your project color */
|
||||
--borders: var(--light3);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Var';
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
font-style: oblique 0deg 10deg;
|
||||
src: url("fonts/Inter.var.woff2?v=3.19") format("woff2");
|
||||
}
|
||||
|
||||
* {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-family: "Inter Var", sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #241f31;
|
||||
background-color: #f6f5f4;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 2rem 0 -6px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
font-variation-settings: "wght" 600;
|
||||
/* needed for webkit */
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
margin: 3rem 0 2rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 650px) {
|
||||
h1 {
|
||||
font-size: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--dark5);
|
||||
}
|
||||
|
||||
img,
|
||||
video,
|
||||
iframe {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
font-variation-settings: "wght" 600;
|
||||
/* needed for webkit */
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 3rem auto 4rem;
|
||||
width: 40%;
|
||||
opacity: 40%;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img.pixels {
|
||||
image-rendering: crisp-edges;
|
||||
/* older firefox browsers */
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
width: 80%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
/* Singletons */
|
||||
#logo {
|
||||
display: block;
|
||||
width: 35%;
|
||||
height: 35%;
|
||||
color: rgba(0, 0, 0, 0);
|
||||
/* make text transparent */
|
||||
background: url('../icons/org.freedesktop.dabrain34.GstPipelineStudio.ico') no-repeat center;
|
||||
padding: 5rem 0 3rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-logo>img {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
a.page-logo {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
/* uninvert */
|
||||
background-image: url('assets/page-logo-i.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.brand-white {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.brand-green {
|
||||
background-color: #30D475;
|
||||
}
|
||||
|
||||
.brand-black {
|
||||
background-color: #201A26;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-link::after {
|
||||
content: " ➜";
|
||||
}
|
||||
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 3em 0 3em;
|
||||
font-size: 1em;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
/* Make tables vertically aligned to the top */
|
||||
tbody td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Github Code Highlighting */
|
||||
.highlight table td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.highlight table pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.highlight .cm {
|
||||
color: #999988;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .cp {
|
||||
color: #999999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .c1 {
|
||||
color: #999988;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .cs {
|
||||
color: #999999;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .c,
|
||||
.highlight .ch,
|
||||
.highlight .cd,
|
||||
.highlight .cpf {
|
||||
color: #999988;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .err {
|
||||
color: #a61717;
|
||||
background-color: #e3d2d2;
|
||||
}
|
||||
|
||||
.highlight .gd {
|
||||
color: #000000;
|
||||
background-color: #ffdddd;
|
||||
}
|
||||
|
||||
.highlight .ge {
|
||||
color: #000000;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .gr {
|
||||
color: #aa0000;
|
||||
}
|
||||
|
||||
.highlight .gh {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.highlight .gi {
|
||||
color: #000000;
|
||||
background-color: #ddffdd;
|
||||
}
|
||||
|
||||
.highlight .go {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.highlight .gp {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.highlight .gs {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .gu {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
||||
.highlight .gt {
|
||||
color: #aa0000;
|
||||
}
|
||||
|
||||
.highlight .kc {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kd {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kn {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kp {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kr {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kt {
|
||||
color: #445588;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .k,
|
||||
.highlight .kv {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .mf {
|
||||
color: #009999;
|
||||
}
|
||||
|
||||
.highlight .mh {
|
||||
color: #009999;
|
||||
}
|
||||
|
||||
.highlight .il {
|
||||
color: #009999;
|
||||
}
|
||||
|
||||
.highlight .mi {
|
||||
color: #009999;
|
||||
}
|
||||
|
||||
.highlight .mo {
|
||||
color: #009999;
|
||||
}
|
||||
|
||||
.highlight .m,
|
||||
.highlight .mb,
|
||||
.highlight .mx {
|
||||
color: #009999;
|
||||
}
|
||||
|
||||
.highlight .sb {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sc {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sd {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .s2 {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .se {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sh {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .si {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sx {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sr {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.highlight .s1 {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .ss {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.highlight .s,
|
||||
.highlight .sa,
|
||||
.highlight .dl {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .na {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .bp {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.highlight .nb {
|
||||
color: #0086B3;
|
||||
}
|
||||
|
||||
.highlight .nc {
|
||||
color: #445588;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .no {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .nd {
|
||||
color: #3c5d5d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .ni {
|
||||
color: #800080;
|
||||
}
|
||||
|
||||
.highlight .ne {
|
||||
color: #990000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .nf,
|
||||
.highlight .fm {
|
||||
color: #990000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .nl {
|
||||
color: #990000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .nn {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.highlight .nt {
|
||||
color: #000080;
|
||||
}
|
||||
|
||||
.highlight .vc {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .vg {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .vi {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .nv,
|
||||
.highlight .vm {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .ow {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .o {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .w {
|
||||
color: #bbbbbb;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
|
||||
/* Code Blocks */
|
||||
.highlighter-rouge {
|
||||
padding: 2px 1rem;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.highlighter-rouge * {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Inline Code */
|
||||
code.highlighter-rouge {
|
||||
padding: 2px 6px;
|
||||
background-color: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-top: 6rem;
|
||||
}
|
||||
|
||||
.inline-button {
|
||||
display: inline-block;
|
||||
font-weight: 900;
|
||||
font-size: 90%;
|
||||
padding: .4rem 1rem;
|
||||
border-radius: var(--rounded-corner);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: var(--dark5);
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
<caption>Composition</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio</url>
|
||||
<url type="homepage">https://dabrain34.pages.freedesktop.org/GstPipelineStudio</url>
|
||||
<url type="bugtracker">https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/issues</url>
|
||||
<categories>
|
||||
<category>Audio</category>
|
||||
|
@ -27,7 +27,17 @@
|
|||
<translation type="gettext">@gettext-package@</translation>
|
||||
<launchable type="desktop-id">@app-id@.desktop</launchable>
|
||||
<releases>
|
||||
<release version="@version@" date="@current_date@"/>
|
||||
<release version="@version@" date="@current_date@">
|
||||
<description>
|
||||
<p>Welcome to GstPipelineStudio</p>
|
||||
<ul>
|
||||
<li>logs: receive multiple log sources such as GST logs and messages.</li>
|
||||
<li>settings: add a log level selection</li>
|
||||
<li>rename gst_pipeline_studio to gst-pipeline-studio</li>
|
||||
<li>can open a pipeline from the command line</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
</releases>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="violence-cartoon">none</content_attribute>
|
||||
|
|
|
@ -3,7 +3,7 @@ Name=GstPipelineStudio
|
|||
GenericName=GPS
|
||||
Comment=A GUI for GStreamer
|
||||
Type=Application
|
||||
Exec=gst_pipeline_studio
|
||||
Exec=gst-pipeline-studio
|
||||
Terminal=false
|
||||
Categories=AudioVideo;Audio;Video;Midi;Settings;GNOME;GTK;
|
||||
Icon=@icon@
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 210 KiB |
53
index.html
Normal file
53
index.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
<html>
|
||||
< <head>
|
||||
<title>GstPipelineStudio</title>
|
||||
<link rel="stylesheet" href="data/css/style.css">
|
||||
<link rel="icon" href="data/icons/org.freedesktop.dabrain34.GstPipelineStudio.ico">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 id="logo"></h1>
|
||||
<h1>GstPipelineStudio: Draw your own pipeline ...</h1>
|
||||
<p><img src="data/screenshots/gps_screenshot.png" alt=""></p>
|
||||
<p>GstPipelineStudio aims to provide a graphical user interface to the GStreamer framework. From a first
|
||||
step in the framework with a simple pipeline to a complex pipeline debugging, the tool provides a
|
||||
friendly interface to add elements to a pipeline and debug it.</p>
|
||||
<h2>GstPipelineStudio 0.3.5 is out, checkout the <a
|
||||
href="https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/-/blob/main/ChangeLog.md?ref_type=heads#anchor-033">
|
||||
Release Notes</a> !
|
||||
<br>
|
||||
<br>
|
||||
<p>Download:</p>
|
||||
<ul>
|
||||
<li><a href="https://flathub.org/apps/org.freedesktop.dabrain34.GstPipelineStudio">Flathub</a></li>
|
||||
<li><a
|
||||
href="https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/uploads/0ab4098a9385f21d72881d27530e6e27/GstPipelineStudio-0.3.5.msi">Windows
|
||||
MSI</a>
|
||||
|
||||
</li>
|
||||
<li><a
|
||||
href="https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/uploads/d6d703809e3023de04e7ad1449fcb4aa/GstPipelineStudio-0.3.5.dmg">MacOS</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="getting-gps">Contributing to GstPipelineStudio</h2>
|
||||
<p>If you want to get the the code from Gitlab, you can visit this <a
|
||||
href="https://flathub.org/apps/org.freedesktop.dabrain34.GstPipelineStudio">webpage</a>
|
||||
and follow the <a
|
||||
href="https://flathub.org/apps/org.freedesktop.dabrain34.GstPipelineStudio/README.md">README</a>
|
||||
to build your own version of GstPipelineStudio.
|
||||
</p>
|
||||
|
||||
|
||||
</h2>
|
||||
</body>
|
||||
<footer class="site-footer">
|
||||
<p>© GstPipelineStudio, 2023</p>
|
||||
|
||||
<p><a href="https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio">Website source</a></p>
|
||||
</footer>
|
||||
|
||||
|
||||
</html>
|
|
@ -9,7 +9,7 @@ test_ok() {
|
|||
|
||||
}
|
||||
|
||||
# depenency library:
|
||||
# dependency library:
|
||||
# Make a .app file: https://gist.github.com/oubiwann/453744744da1141ccc542ff75b47e0cf
|
||||
# Make a .dmg file: https://github.com/LinusU/node-appdmg
|
||||
# Can't find library: https://www.jianshu.com/p/441a7553700f
|
||||
|
@ -25,14 +25,15 @@ GSTREAMER_OPTS="
|
|||
-Dforce_fallback_for=gstreamer-1.0,libffi,pcre2 \
|
||||
-Dgstreamer-1.0:libav=disabled \
|
||||
-Dgstreamer-1.0:examples=disabled \
|
||||
-Dgstreamer-1.0:introspection=disabled \
|
||||
-Dgstreamer-1.0:introspection=disabled \
|
||||
-Dgstreamer-1.0:rtsp_server=disabled \
|
||||
-Dgstreamer-1.0:devtools=disabled \
|
||||
-Dgst-plugins-base:tests=disabled \
|
||||
-Dgstreamer-1.0:tests=disabled \
|
||||
-Dgst-plugins-bad:openexr=disabled -Dgstreamer-1.0:gst-examples=disabled \
|
||||
-Dorc:gtk_doc=disabled \
|
||||
-Dgstreamer-1.0:python=disabled"
|
||||
-Dgst-plugins-base:tests=disabled \
|
||||
-Dgstreamer-1.0:tests=disabled \
|
||||
-Dgst-plugins-bad:openexr=disabled -Dgstreamer-1.0:gst-examples=disabled \
|
||||
-Dorc:gtk_doc=disabled \
|
||||
-Dgstreamer-1.0:ges=disabled \
|
||||
-Dgstreamer-1.0:python=disabled"
|
||||
|
||||
# rebuild app release version
|
||||
rm -rf "${TARGETDIR}"
|
||||
|
@ -64,7 +65,7 @@ function lib_dependency_copy
|
|||
if [[ '@loader_path' == ${lib:0:12} ]]; then
|
||||
cp -n "${lib/@loader_path/$lib_dir}" $folder
|
||||
else
|
||||
echo "Unsupport path: $lib"
|
||||
echo "Unsupported path: $lib"
|
||||
fi
|
||||
else
|
||||
if [[ $lib != $target ]]; then
|
||||
|
@ -93,10 +94,10 @@ function lib_dependency_analyze
|
|||
# copy app dependency library to target dir
|
||||
echo -n "Copy app dependency library......"
|
||||
|
||||
|
||||
lib_dependency_copy ${TARGETDIR}/bin/gst_pipeline_studio "${TARGETDIR}/bin"
|
||||
lib_dependency_copy ${TARGETDIR}/bin/gst-pipeline-studio "${TARGETDIR}/bin"
|
||||
lib_dependency_copy ${TARGETDIR}/lib/libgobject-2.0.0.dylib "${TARGETDIR}/bin"
|
||||
lib_dependency_copy ${TARGETDIR}/lib/libsoup-2.4.1.dylib "${TARGETDIR}/bin"
|
||||
lib_dependency_copy "${TARGETDIR}/bin/libgtk-4.1.dylib" "${TARGETDIR}/bin"
|
||||
|
||||
|
||||
for file in ${TARGETDIR}/lib/gstreamer-1.0/*.dylib
|
||||
|
@ -107,12 +108,7 @@ done
|
|||
|
||||
test_ok cp -f "${PROJECTDIR}/macos/mac_launcher.sh" "${TARGETDIR}/bin/launcher.sh"
|
||||
|
||||
|
||||
# copy GStreamer dependencies
|
||||
# cp -f /usr/local/lib/gstreamer-1.0/libgtk-gtk4.1.dylib "${TARGETDIR}/lib/gstreamer-1.0"
|
||||
|
||||
|
||||
|
||||
# FIXME should build and install gtk4 instead of using homebrew
|
||||
|
||||
# copy GTK runtime dependencies resource
|
||||
# echo -n "Copy GTK runtime resource......"
|
||||
|
@ -122,8 +118,9 @@ test_ok cp -f "${PROJECTDIR}/macos/mac_launcher.sh" "${TARGETDIR}/bin/launcher.s
|
|||
# cp -rf /usr/local/lib/girepository-1.0 "${TARGETDIR}/lib/"
|
||||
# cp -rf /usr/local/lib/libgda-5.0 "${TARGETDIR}/lib/"
|
||||
# # Avoid override the latest locale file
|
||||
# cp -r /usr/local/share/locale "${TARGETDIR}/share/"
|
||||
# cp -rf /usr/local/share/icons "${TARGETDIR}/share/"
|
||||
cp -r /opt/homebrew/share/locale "${TARGETDIR}/share/"
|
||||
cp -rf /opt/homebrew/share/icons "${TARGETDIR}/share/"
|
||||
cp -rf /opt/homebrew/share/gtk4-0 "${TARGETDIR}/share/"
|
||||
# cp -rf /usr/local/share/fontconfig "${TARGETDIR}/share/"
|
||||
# cp -rf /usr/local/share/themes/Mac "${TARGETDIR}/share/themes/"
|
||||
# cp -rf /usr/local/share/themes/Default "${TARGETDIR}/share/themes/"
|
||||
|
@ -131,9 +128,9 @@ test_ok cp -f "${PROJECTDIR}/macos/mac_launcher.sh" "${TARGETDIR}/bin/launcher.s
|
|||
# glib-compile-schemas /usr/local/share/glib-2.0/schemas
|
||||
# cp -f /usr/local/share/glib-2.0/schemas/gschema* "${TARGETDIR}/share/glib-2.0/schemas"
|
||||
# # find "${TARGETDIR}/bin" -type f -path '*.dll.a' -exec rm '{}' \;
|
||||
# lib_dependency_analyze ${TARGETDIR}/lib ${TARGETDIR}/bin
|
||||
# lib_dependency_analyze ${TARGETDIR}/bin ${TARGETDIR}/bin
|
||||
# echo "[done]"
|
||||
lib_dependency_analyze ${TARGETDIR}/lib ${TARGETDIR}/bin
|
||||
lib_dependency_analyze ${TARGETDIR}/bin ${TARGETDIR}/bin
|
||||
echo "[done]"
|
||||
|
||||
# copy app icons and license files to target dir
|
||||
echo -n "Copy app icon(svg) files......"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<key>GtkOSXLaunchScriptFile</key>
|
||||
<string>launcher.sh</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>gst_pipeline_studio</string>
|
||||
<string>gst-pipeline-studio</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>GstPipelineStudio.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
@ -38,6 +38,6 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -93,10 +93,10 @@ cp -rf "$APP_BUILD/etc" "$APP_RES_DIR"
|
|||
cp -rf "$APP_BUILD/lib" "$APP_RES_DIR"
|
||||
cp -rf "$APP_BUILD/share" "$APP_RES_DIR"
|
||||
cp -rf "$APP_BUILD/libexec" "$APP_RES_DIR"
|
||||
cp $APP_BUILD/bin/gst_pipeline_studio $APP_EXE_DIR/gst_pipeline_studio-real
|
||||
cp $APP_BUILD/bin/launcher.sh $APP_EXE_DIR/gst_pipeline_studio
|
||||
chmod 766 "$APP_EXE_DIR/gst_pipeline_studio"
|
||||
chmod 766 "$APP_EXE_DIR/gst_pipeline_studio-real"
|
||||
cp $APP_BUILD/bin/gst-pipeline-studio $APP_EXE_DIR/gst-pipeline-studio-real
|
||||
cp $APP_BUILD/bin/launcher.sh $APP_EXE_DIR/gst-pipeline-studio
|
||||
chmod 766 "$APP_EXE_DIR/gst-pipeline-studio"
|
||||
chmod 766 "$APP_EXE_DIR/gst-pipeline-studio-real"
|
||||
chmod -R 766 "$APP_RES_DIR"/libexec/gstreamer-1.0
|
||||
|
||||
|
||||
|
@ -123,7 +123,7 @@ done
|
|||
lib_change_paths \
|
||||
@executable_path/../Resources/lib \
|
||||
$APP_LIB_DIR \
|
||||
$APP_EXE_DIR/gst_pipeline_studio-real
|
||||
$APP_EXE_DIR/gst-pipeline-studio-real
|
||||
|
||||
|
||||
lib_change_siblings $APP_LIB_DIR @loader_path
|
||||
|
|
|
@ -31,7 +31,7 @@ function lib_change_path
|
|||
# This is a simple wrapper around install_name_tool to reduce the
|
||||
# number of arguments (like $source does not have to be provided
|
||||
# here as it can be deducted from $target).
|
||||
# Also, the requested change can be applied to multipe binaries
|
||||
# Also, the requested change can be applied to multiple binaries
|
||||
# at once since 2-n arguments can be supplied.
|
||||
|
||||
local target=$1 # new path to dynamically linked library
|
||||
|
@ -99,7 +99,7 @@ function lib_change_siblings
|
|||
lib_change_path $target/$(basename $linked_lib) $lib
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
@ -167,4 +167,4 @@ if /bin/expr "x$1" : '^x-psn_' > /dev/null; then
|
|||
shift 1
|
||||
fi
|
||||
|
||||
$EXEC "$bundle_contents/MacOS/gst_pipeline_studio-real" "$@" $EXTRA_ARGS
|
||||
$EXEC "$bundle_contents/MacOS/gst-pipeline-studio-real" "$@" $EXTRA_ARGS
|
||||
|
|
|
@ -78,7 +78,7 @@ This is a tricky question. We believe software patents should not exist, so that
|
|||
\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af7\langfe2052\dbch\af8\afs24\alang1081\loch\f3\fs24\lang1031\sl276\slmult1\sb0\sa200{\b0\rtlch \ltrch\loch\fs24\lang255\loch\f6
|
||||
Software patents are widely available in the USA. Despite they are formally prohibited in the European Union, they indeed are granted by the thousand by the European Patent Office, and also some national patent offices follow the same path. In other countries they are not available.}
|
||||
\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af7\langfe2052\dbch\af8\afs24\alang1081\loch\f3\fs24\lang1031\sl276\slmult1\sb0\sa200{\b0\rtlch \ltrch\loch\fs24\lang255\loch\f6
|
||||
Since patent protection is a national state-granted monopoly, distributing software that violates patents in a given country could be entirely safe if done in another country. Fair use exceptions also exist. So we cannot advice you whether the software we provide would be considered violating patents in your country or in any other country, but that can be said for virtually all kinds of sofware. Only, since we deal with audio-video standards, and these standards are by and large designed to use certain patented technologies, it is common wisdom that the pieces of software that implement these standards are sensitive in this respect.}
|
||||
Since patent protection is a national state-granted monopoly, distributing software that violates patents in a given country could be entirely safe if done in another country. Fair use exceptions also exist. So we cannot advice you whether the software we provide would be considered violating patents in your country or in any other country, but that can be said for virtually all kinds of software. Only, since we deal with audio-video standards, and these standards are by and large designed to use certain patented technologies, it is common wisdom that the pieces of software that implement these standards are sensitive in this respect.}
|
||||
\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af7\langfe2052\dbch\af8\afs24\alang1081\loch\f3\fs24\lang1031\sl276\slmult1\sb0\sa200{\b0\rtlch \ltrch\loch\fs24\lang255\loch\f6
|
||||
This is why GStreamer has taken a modular approach, so that you can use a Free plugins or a proprietary, patent royalty bearing, plugin for a given standard.}
|
||||
\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af7\langfe2052\dbch\af8\afs24\alang1081\loch\f3\fs24\lang1031\sl276\slmult1\sb0\sa200{\b\rtlch \ltrch\loch\fs36\lang255\loch\f6
|
||||
|
|
|
@ -33,13 +33,15 @@ try
|
|||
# GST and GTK are installed in this folder by prepare_gstreamer.ps1.
|
||||
# GST and GTK are built by the docker image.
|
||||
$gstreamerInstallDir="c:\gst-install-clean"
|
||||
$gstreamerBinInstallDir= Join-Path $gstreamerInstallDir -ChildPath "bin/"
|
||||
$gstreamerPluginInstallDir= Join-Path $gstreamerInstallDir -ChildPath "lib\gstreamer-1.0"
|
||||
$gstreamerBinInstallDir= Join-Path $gstreamerInstallDir -ChildPath "bin"
|
||||
$gstreamerPluginInstallDir= Join-Path $gstreamerInstallDir -ChildPath "lib"
|
||||
$gstreamerShareInstallDir= Join-Path $gstreamerInstallDir -ChildPath "share"
|
||||
|
||||
& "$heatToolPath" dir "$gstreamerBinInstallDir" -gg -sfrag -template:fragment -out gstreamer-1.0.wxs -cg "_gstreamer" -var var.gstreamerBinInstallDir -dr INSTALLFOLDER
|
||||
& "$heatToolPath" dir "$gstreamerPluginInstallDir" -gg -sfrag -template:fragment -out gstreamer-plugins-1.0.wxs -cg "_gstreamer_plugins" -var var.gstreamerPluginInstallDir -dr INSTALLFOLDER
|
||||
& "$heatToolPath" dir "$gstreamerShareInstallDir" -v -ke -gg -sfrag -template:fragment -out gstreamer-share-1.0.wxs -cg "_gstreamer_share" -var var.gstreamerShareInstallDir -dr INSTALLFOLDER
|
||||
|
||||
$files = "gps gstreamer-1.0 gstreamer-plugins-1.0"
|
||||
$files = "gps gstreamer-1.0 gstreamer-plugins-1.0 gstreamer-share-1.0"
|
||||
$wxs_files = @()
|
||||
$obj_files = @()
|
||||
foreach ($f in $files.split(" ")){
|
||||
|
@ -51,7 +53,7 @@ try
|
|||
# compiling wxs file into wixobj
|
||||
$msiFileName = "GstPipelineStudio-$GPSVersion.msi"
|
||||
foreach ($f in $wxs_files){
|
||||
& "$candleToolPath" "$f" -dPlatform=x64 -dGPSUpgradeCode="$GPSUpgradeCode" -dGPSVersion="$GPSVersion" -dgstreamerBinInstallDir="$gstreamerBinInstallDir" -dgstreamerPluginInstallDir="$gstreamerPluginInstallDir"
|
||||
& "$candleToolPath" "$f" -dPlatform=x64 -dGPSUpgradeCode="$GPSUpgradeCode" -dGPSVersion="$GPSVersion" -dgstreamerBinInstallDir="$gstreamerBinInstallDir" -dgstreamerPluginInstallDir="$gstreamerPluginInstallDir" -dgstreamerShareInstallDir="$gstreamerShareInstallDir"
|
||||
if($LASTEXITCODE -ne 0)
|
||||
{
|
||||
throw "Compilation of $wxsFileName failed with exit code $LASTEXITCODE"
|
||||
|
|
|
@ -3,6 +3,6 @@ set MYDIR=%~dp0
|
|||
setlocal
|
||||
set PATH=%MYDIR%bin;%PATH%
|
||||
echo %PATH%
|
||||
set GST_PLUGIN_PATH=%MYDIR%/gstreamer-1.0
|
||||
set GST_PLUGIN_PATH=%MYDIR%\lib\gstreamer-1.0
|
||||
echo %GST_PLUGIN_PATH%
|
||||
gst_pipeline_studio.exe
|
||||
gst-pipeline-studio.exe
|
|
@ -10,10 +10,12 @@
|
|||
<Package InstallScope="perMachine" Compressed="yes" />
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
|
||||
<WixVariable Id="WixUIBannerBmp" Value="wixbanner.bmp" />
|
||||
<WixVariable Id="WixUIDialogBmp" Value="wixdialog.bmp" />
|
||||
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<UIRef Id="WixUI_Mondo" />
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
<WixVariable Id="WixUILicenseRtf" Value="LICENSE.rtf" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
|
@ -59,6 +61,7 @@
|
|||
<ComponentRef Id="ProductComponent" />
|
||||
<ComponentGroupRef Id="_gstreamer" />
|
||||
<ComponentGroupRef Id="_gstreamer_plugins" />
|
||||
<ComponentGroupRef Id="_gstreamer_share" />
|
||||
<ComponentRef Id="UninstallShortcut" />
|
||||
</Feature>
|
||||
|
||||
|
|
|
@ -9,3 +9,4 @@ Copy-Item -Path C:\gst-install\bin\*.exe -Destination c:\gst-install-clean\bin\
|
|||
New-Item c:\gst-install-clean\lib\gstreamer-1.0 -ItemType Directory
|
||||
Copy-Item -Path C:\gst-install\lib\gstreamer-1.0\*.dll -Destination c:\gst-install-clean\lib\gstreamer-1.0
|
||||
|
||||
Copy-Item -Path C:\gst-install\share -Destination c:\gst-install-clean\ -Recurse
|
||||
|
|
BIN
installer/wix/wixbanner.bmp
Normal file
BIN
installer/wix/wixbanner.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
BIN
installer/wix/wixdialog.bmp
Normal file
BIN
installer/wix/wixdialog.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 507 KiB |
12
meson.build
12
meson.build
|
@ -1,6 +1,6 @@
|
|||
project('gst_pipeline_studio',
|
||||
version: '0.3.2',
|
||||
meson_version: '>= 0.50.0',
|
||||
project('gst-pipeline-studio',
|
||||
version: '0.3.5',
|
||||
meson_version: '>= 0.63.0',
|
||||
default_options: [ 'warning_level=2',
|
||||
],
|
||||
license: 'GPL-3.0-or-later',
|
||||
|
@ -36,7 +36,7 @@ localedir = prefix / get_option('localedir')
|
|||
datadir = prefix / get_option('datadir')
|
||||
pkgdatadir = datadir / meson.project_name()
|
||||
iconsdir = datadir / 'icons'
|
||||
podir = meson.source_root () / 'po'
|
||||
podir = meson.project_source_root () / 'po'
|
||||
gettext_package = meson.project_name()
|
||||
base_id = 'org.freedesktop.dabrain34.GstPipelineStudio'
|
||||
|
||||
|
@ -70,8 +70,8 @@ subdir('po')
|
|||
|
||||
meson.add_dist_script(
|
||||
'build-aux/dist-vendor.sh',
|
||||
meson.source_root(),
|
||||
join_paths(meson.build_root(), 'meson-dist', meson.project_name() + '-' + version)
|
||||
meson.project_source_root(),
|
||||
join_paths(meson.project_build_root(), 'meson-dist', meson.project_name() + '-' + version)
|
||||
)
|
||||
if host_system == 'linux'
|
||||
meson.add_install_script('build-aux/meson/postinstall.py')
|
||||
|
|
17
release.md
17
release.md
|
@ -3,15 +3,22 @@
|
|||
- Update to the given version:
|
||||
- meson.build
|
||||
- cargo.toml
|
||||
- installer/wix/build_installer.ps1
|
||||
- VERSION
|
||||
- index.html
|
||||
- And rebuild to regenerate the cargo.lock
|
||||
- update the changelog in org.freedesktop.dabrain34.GstPipelineStudio.appdata.xml.in.in within release/description
|
||||
|
||||
- create a tag on gitlab
|
||||
- meson builddir -Dbuildtype=release
|
||||
- ninja -C builddir dist
|
||||
- upload the package and the sha256 to gitlab for Flatpak in the release notes
|
||||
- Fetch the package from the `linux release` job or you can make it manually with:
|
||||
- meson builddir -Dbuildtype=release
|
||||
- ninja -C builddir dist
|
||||
- Upload the package .xz file and the sha256 to gitlab release page in the release notes
|
||||
see https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/-/releases/0.3.2
|
||||
|
||||
# flathub
|
||||
|
||||
https://github.com/flathub/org.freedesktop.dabrain34.GstPipelineStudio
|
||||
|
||||
- Need to update the package and the sha256 generated by ninja -C builddir dist
|
||||
- Need to update the package and the sha256 from the release page, ie https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/-/releases/0.3.2
|
||||
- Create a pull request with the package update
|
||||
- Wait at lest 2-3 hours after merging to get the update available.
|
||||
|
|
79
src/app.rs
79
src/app.rs
|
@ -105,12 +105,14 @@ impl GPSApp {
|
|||
}));
|
||||
let settings = Settings::load_settings();
|
||||
|
||||
app.window
|
||||
.set_default_size(settings.app_width, settings.app_height);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if settings.app_maximized {
|
||||
app.window.maximize();
|
||||
} else {
|
||||
app.window
|
||||
.set_size_request(settings.app_width, settings.app_height);
|
||||
}
|
||||
|
||||
app.set_paned_position(&settings, "graph_dashboard-paned", 100);
|
||||
app.set_paned_position(&settings, "graph_logs-paned", 100);
|
||||
app.set_paned_position(&settings, "elements_preview-paned", 100);
|
||||
|
@ -148,7 +150,7 @@ impl GPSApp {
|
|||
.insert(paned_name.to_string(), paned.position());
|
||||
}
|
||||
|
||||
pub fn on_startup(application: >k::Application) {
|
||||
pub fn on_startup(application: >k::Application, pipeline_desc: &String) {
|
||||
// Create application and error out if that fails for whatever reason
|
||||
let app = match GPSApp::new(application) {
|
||||
Ok(app) => app,
|
||||
|
@ -158,13 +160,7 @@ impl GPSApp {
|
|||
}
|
||||
};
|
||||
|
||||
// When the application is activated show the UI. This happens when the first process is
|
||||
// started, and in the first process whenever a second process is started
|
||||
let app_weak = app.downgrade();
|
||||
application.connect_activate(glib::clone!(@weak application => move |_| {
|
||||
let app = upgrade_weak!(app_weak);
|
||||
app.build_ui(&application);
|
||||
}));
|
||||
app.build_ui(application, pipeline_desc);
|
||||
|
||||
let app_weak = app.downgrade();
|
||||
let slider: gtk::Scale = app
|
||||
|
@ -227,8 +223,8 @@ impl GPSApp {
|
|||
.expect("Couldn't get the main window");
|
||||
let mut settings = Settings::load_settings();
|
||||
settings.app_maximized = window.is_maximized();
|
||||
settings.app_width = window.width();
|
||||
settings.app_height = window.height();
|
||||
settings.app_width = window.default_width();
|
||||
settings.app_height = window.default_height();
|
||||
app.save_paned_position(&mut settings, "graph_dashboard-paned");
|
||||
app.save_paned_position(&mut settings, "graph_logs-paned");
|
||||
app.save_paned_position(&mut settings, "elements_preview-paned");
|
||||
|
@ -447,23 +443,29 @@ impl GPSApp {
|
|||
notebook_preview.set_current_page(Some(n_video_sink as u32));
|
||||
}
|
||||
|
||||
pub fn build_ui(&self, application: &Application) {
|
||||
pub fn build_ui(&self, application: &Application, pipeline_desc: &String) {
|
||||
graphbook::setup_graphbook(self);
|
||||
graphbook::create_graphtab(self, 0, None);
|
||||
let (ready_tx, ready_rx) = async_channel::unbounded::<(logger::LogType, String)>();
|
||||
|
||||
// Setup the logger to get messages into the TreeView
|
||||
let (ready_tx, ready_rx) = glib::MainContext::channel(glib::Priority::DEFAULT);
|
||||
let app_weak = self.downgrade();
|
||||
|
||||
logger::init_logger(
|
||||
ready_tx,
|
||||
Settings::default_log_file_path()
|
||||
ready_tx.clone(),
|
||||
Settings::log_file_path()
|
||||
.to_str()
|
||||
.expect("Unable to convert log file path to a string"),
|
||||
);
|
||||
GPSUI::logger::setup_logger_list(self);
|
||||
let _ = ready_rx.attach(None, move |msg: String| {
|
||||
let app = upgrade_weak!(app_weak, glib::ControlFlow::Break);
|
||||
GPSUI::logger::add_to_logger_list(&app, &msg);
|
||||
logger::init_msg_logger(ready_tx);
|
||||
GPSUI::logger::setup_logger_list(self, "treeview-app-logger", logger::LogType::App);
|
||||
GPSUI::logger::setup_logger_list(self, "treeview-msg-logger", logger::LogType::Message);
|
||||
GPSUI::logger::setup_logger_list(self, "treeview-gst-logger", logger::LogType::Gst);
|
||||
let app_weak = self.downgrade();
|
||||
glib::spawn_future_local(async move {
|
||||
while let Ok(msg) = ready_rx.recv().await {
|
||||
let app = upgrade_weak!(app_weak, glib::ControlFlow::Break);
|
||||
GPSUI::logger::add_to_logger_list(&app, msg.0, &msg.1);
|
||||
}
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
|
||||
|
@ -509,8 +511,9 @@ impl GPSApp {
|
|||
&Settings::recent_pipeline_description(),
|
||||
&app,
|
||||
move |app, pipeline_desc| {
|
||||
app.load_pipeline(&pipeline_desc)
|
||||
.unwrap_or_else(|_| GPS_ERROR!("Unable to open file {}", pipeline_desc));
|
||||
app.load_pipeline(&pipeline_desc).unwrap_or_else(|_| {
|
||||
GPS_ERROR!("Unable to open pipeline description {}", pipeline_desc)
|
||||
});
|
||||
Settings::set_recent_pipeline_description(&pipeline_desc);
|
||||
},
|
||||
);
|
||||
|
@ -602,17 +605,22 @@ impl GPSApp {
|
|||
GPSUI::elements::setup_favorite_list(self);
|
||||
// Setup the favorite list
|
||||
GPSUI::elements::setup_elements_list(self);
|
||||
|
||||
let _ = self
|
||||
.load_graph(
|
||||
Settings::default_graph_file_path()
|
||||
.to_str()
|
||||
.expect("Unable to convert to string"),
|
||||
true,
|
||||
)
|
||||
.map_err(|_e| {
|
||||
GPS_WARN!("Unable to load default graph");
|
||||
if pipeline_desc.is_empty() {
|
||||
let _ = self
|
||||
.load_graph(
|
||||
Settings::graph_file_path()
|
||||
.to_str()
|
||||
.expect("Unable to convert to string"),
|
||||
true,
|
||||
)
|
||||
.map_err(|_e| {
|
||||
GPS_WARN!("Unable to load default graph");
|
||||
});
|
||||
} else {
|
||||
self.load_pipeline(pipeline_desc).unwrap_or_else(|_| {
|
||||
GPS_ERROR!("Unable to open pipeline description {}", pipeline_desc)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Downgrade to a weak reference
|
||||
|
@ -773,9 +781,10 @@ impl GPSApp {
|
|||
|
||||
fn load_pipeline(&self, pipeline_desc: &str) -> anyhow::Result<()> {
|
||||
let graphtab = graphbook::current_graphtab(self);
|
||||
let pd_parsed = pipeline_desc.replace('\\', "");
|
||||
graphtab
|
||||
.player()
|
||||
.graphview_from_pipeline_description(&graphtab.graphview(), pipeline_desc);
|
||||
.graphview_from_pipeline_description(&graphtab.graphview(), &pd_parsed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pub static APP_ID: &str = @APP_ID@;
|
||||
pub static VERSION: &str = @VERSION@;
|
||||
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
|
|
@ -156,13 +156,31 @@ impl ElementInfo {
|
|||
}
|
||||
|
||||
pub fn element_property(element: &gst::Element, property_name: &str) -> anyhow::Result<String> {
|
||||
let value = element
|
||||
.property_value(property_name)
|
||||
.transform::<String>()
|
||||
.expect("Unable to transform to string")
|
||||
.get::<String>()
|
||||
.unwrap_or_default();
|
||||
Ok(value)
|
||||
let value = element.property_value(property_name);
|
||||
if value.type_().is_a(glib::Type::ENUM) {
|
||||
let value = value.get::<&glib::EnumValue>().unwrap().nick().to_string();
|
||||
Ok(value)
|
||||
} else if value.type_().is_a(glib::Type::FLAGS) {
|
||||
let value = value.get::<Vec<&glib::FlagsValue>>().unwrap();
|
||||
let flags = value.iter().copied().fold(0, |acc, val| acc | val.value());
|
||||
Ok(flags.to_string())
|
||||
} else if value.type_().is_a(glib::Type::F64) || value.type_().is_a(glib::Type::F32) {
|
||||
let value = value
|
||||
.transform::<String>()
|
||||
.expect("Unable to transform to string")
|
||||
.get::<String>()
|
||||
.unwrap()
|
||||
.replace(',', ".");
|
||||
Ok(value)
|
||||
} else {
|
||||
let value = value
|
||||
.transform::<String>()
|
||||
.expect("Unable to transform to string")
|
||||
.get::<String>()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element_property_by_feature_name(
|
||||
|
@ -181,7 +199,7 @@ impl ElementInfo {
|
|||
element: &gst::Element,
|
||||
) -> anyhow::Result<HashMap<String, glib::ParamSpec>> {
|
||||
let mut properties_list = HashMap::new();
|
||||
let params = element.class().list_properties();
|
||||
let params = element.list_properties();
|
||||
|
||||
for param in params.iter() {
|
||||
GPS_INFO!("Property_name {}", param.name());
|
||||
|
@ -258,7 +276,7 @@ impl ElementInfo {
|
|||
None
|
||||
}
|
||||
|
||||
pub fn search_fo_element(bin: &gst::Bin, element_name: &str) -> Vec<gst::Element> {
|
||||
pub fn search_for_element(bin: &gst::Bin, element_name: &str) -> Vec<gst::Element> {
|
||||
let mut iter = bin.iterate_elements();
|
||||
let mut elements: Vec<gst::Element> = Vec::new();
|
||||
elements = loop {
|
||||
|
@ -266,7 +284,7 @@ impl ElementInfo {
|
|||
Ok(Some(element)) => {
|
||||
if element.is::<gst::Bin>() {
|
||||
let bin = element.dynamic_cast::<gst::Bin>().unwrap();
|
||||
let mut bin_elements = ElementInfo::search_fo_element(&bin, element_name);
|
||||
let mut bin_elements = ElementInfo::search_for_element(&bin, element_name);
|
||||
elements.append(&mut bin_elements);
|
||||
} else {
|
||||
GPS_INFO!("Found factory: {}", element.factory().unwrap().name());
|
||||
|
|
|
@ -26,10 +26,11 @@ use std::fmt::Write as _;
|
|||
use std::ops;
|
||||
use std::rc::{Rc, Weak};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PipelineState {
|
||||
Playing,
|
||||
Paused,
|
||||
#[default]
|
||||
Stopped,
|
||||
Error,
|
||||
}
|
||||
|
@ -40,7 +41,7 @@ impl fmt::Display for PipelineState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Player(Rc<PlayerInner>);
|
||||
|
||||
// Deref into the contained struct to make usage a bit more ergonomic
|
||||
|
@ -61,12 +62,35 @@ impl PlayerWeak {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
fn gst_log_handler(
|
||||
category: gst::DebugCategory,
|
||||
level: gst::DebugLevel,
|
||||
file: &glib::GStr,
|
||||
function: &glib::GStr,
|
||||
line: u32,
|
||||
_obj: Option<&gst::LoggedObject>,
|
||||
message: &gst::DebugMessage,
|
||||
) {
|
||||
let log_message = format!(
|
||||
"{}\t{}\t{}:{}:{}\t{}",
|
||||
level,
|
||||
category.name(),
|
||||
line,
|
||||
file.as_str(),
|
||||
function.as_str(),
|
||||
message.get().unwrap().as_str()
|
||||
);
|
||||
|
||||
GPS_GST_LOG!("{}", log_message);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PlayerInner {
|
||||
app: RefCell<Option<GPSApp>>,
|
||||
pipeline: RefCell<Option<gst::Pipeline>>,
|
||||
current_state: Cell<PipelineState>,
|
||||
n_video_sink: Cell<usize>,
|
||||
bus_watch_guard: RefCell<Option<gst::bus::BusWatchGuard>>,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
|
@ -76,8 +100,9 @@ impl Player {
|
|||
pipeline: RefCell::new(None),
|
||||
current_state: Cell::new(PipelineState::Stopped),
|
||||
n_video_sink: Cell::new(0),
|
||||
bus_watch_guard: RefCell::new(None),
|
||||
}));
|
||||
|
||||
gst::log::add_log_function(gst_log_handler);
|
||||
Ok(pipeline)
|
||||
}
|
||||
|
||||
|
@ -99,13 +124,13 @@ impl Player {
|
|||
.parse::<bool>()
|
||||
.expect("Should a boolean value")
|
||||
{
|
||||
ElementInfo::element_update_rank("gtk4paintablesink", gst::Rank::Primary);
|
||||
ElementInfo::element_update_rank("gtk4paintablesink", gst::Rank::PRIMARY);
|
||||
} else {
|
||||
ElementInfo::element_update_rank("gtk4paintablesink", gst::Rank::Marginal);
|
||||
ElementInfo::element_update_rank("gtk4paintablesink", gst::Rank::MARGINAL);
|
||||
}
|
||||
|
||||
gst::log::set_threshold_from_string(settings::Settings::gst_log_level().as_str(), true);
|
||||
// Create pipeline from the description
|
||||
let pipeline = gst::parse_launch(description)?;
|
||||
let pipeline = gst::parse::launch(description)?;
|
||||
let pipeline = pipeline.downcast::<gst::Pipeline>();
|
||||
/* start playing */
|
||||
if pipeline.is_err() {
|
||||
|
@ -117,19 +142,21 @@ impl Player {
|
|||
self.check_for_gtk4sink(pipeline.as_ref().unwrap());
|
||||
// GPSApp is not Send(trait) ready , so we use a channel to exchange the given data with the main thread and use
|
||||
// GPSApp.
|
||||
let (ready_tx, ready_rx) = glib::MainContext::channel(glib::Priority::DEFAULT);
|
||||
let (ready_tx, ready_rx) = async_channel::unbounded::<gst::Element>();
|
||||
let player_weak = self.downgrade();
|
||||
let _ = ready_rx.attach(None, move |element: gst::Element| {
|
||||
let player = upgrade_weak!(player_weak, glib::ControlFlow::Break);
|
||||
let paintable = element.property::<gdk::Paintable>("paintable");
|
||||
let n_sink = player.n_video_sink.get();
|
||||
player
|
||||
.app
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.expect("App should be available")
|
||||
.set_app_preview(&paintable, n_sink);
|
||||
player.n_video_sink.set(n_sink + 1);
|
||||
glib::spawn_future_local(async move {
|
||||
while let Ok(element) = ready_rx.recv().await {
|
||||
let player = upgrade_weak!(player_weak, glib::ControlFlow::Break);
|
||||
let paintable = element.property::<gdk::Paintable>("paintable");
|
||||
let n_sink = player.n_video_sink.get();
|
||||
player
|
||||
.app
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.expect("App should be available")
|
||||
.set_app_preview(&paintable, n_sink);
|
||||
player.n_video_sink.set(n_sink + 1);
|
||||
}
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
let bin = pipeline.unwrap().dynamic_cast::<gst::Bin>();
|
||||
|
@ -138,7 +165,7 @@ impl Player {
|
|||
if let Some(factory) = element.factory() {
|
||||
GPS_INFO!("Received the signal deep element added {}", factory.name());
|
||||
if factory.name() == "gtk4paintablesink" {
|
||||
let _ = ready_tx.send(element.clone());
|
||||
let _ = ready_tx.try_send(element.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -149,7 +176,7 @@ impl Player {
|
|||
|
||||
pub fn check_for_gtk4sink(&self, pipeline: &gst::Pipeline) {
|
||||
let bin = pipeline.clone().dynamic_cast::<gst::Bin>().unwrap();
|
||||
let gtksinks = ElementInfo::search_fo_element(&bin, "gtk4paintablesink");
|
||||
let gtksinks = ElementInfo::search_for_element(&bin, "gtk4paintablesink");
|
||||
|
||||
for (first_sink, gtksink) in gtksinks.into_iter().enumerate() {
|
||||
let paintable = gtksink.property::<gdk::Paintable>("paintable");
|
||||
|
@ -176,12 +203,13 @@ impl Player {
|
|||
|
||||
let bus = pipeline.bus().expect("Pipeline had no bus");
|
||||
let pipeline_weak = self.downgrade();
|
||||
let _ = bus.add_watch_local(move |_bus, msg| {
|
||||
let bus_watch_guard = bus.add_watch_local(move |_bus, msg| {
|
||||
let pipeline = upgrade_weak!(pipeline_weak, glib::ControlFlow::Break);
|
||||
pipeline.on_pipeline_message(msg);
|
||||
glib::ControlFlow::Continue
|
||||
})?;
|
||||
*self.pipeline.borrow_mut() = Some(pipeline);
|
||||
*self.bus_watch_guard.borrow_mut() = Some(bus_watch_guard);
|
||||
}
|
||||
|
||||
self.set_state(new_state).map_err(|error| {
|
||||
|
@ -279,6 +307,9 @@ impl Player {
|
|||
|
||||
fn on_pipeline_message(&self, msg: &gst::MessageRef) {
|
||||
use gst::MessageView;
|
||||
if let Some(message) = msg.structure() {
|
||||
GPS_MSG_LOG!("{:?}", message);
|
||||
}
|
||||
match msg.view() {
|
||||
MessageView::Eos(_) => {
|
||||
GPS_INFO!("EOS received");
|
||||
|
@ -317,7 +348,7 @@ impl Player {
|
|||
.unwrap()
|
||||
.dynamic_cast::<gst::Bin>()
|
||||
.unwrap();
|
||||
let elements_name: Vec<String> = ElementInfo::search_fo_element(&bin, "")
|
||||
let elements_name: Vec<String> = ElementInfo::search_for_element(&bin, "")
|
||||
.iter()
|
||||
.map(|e| e.factory().unwrap().name().to_string())
|
||||
.collect();
|
||||
|
|
|
@ -19,13 +19,14 @@ use gtk::{gio, glib, graphene};
|
|||
use std::cell::{Cell, Ref, RefCell};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
enum TabState {
|
||||
#[default]
|
||||
Undefined = 0,
|
||||
Modified,
|
||||
Saved,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GraphTab {
|
||||
graphview: RefCell<GM::GraphView>,
|
||||
player: RefCell<GPS::Player>,
|
||||
|
@ -173,7 +174,7 @@ pub fn setup_graphbook(app: &GPSApp) {
|
|||
graphbook.connect_switch_page(move |_book, widget, page| {
|
||||
let graphview = widget
|
||||
.first_child()
|
||||
.expect("Unable to get the child from the graphbook, ie the graphview");
|
||||
.expect("Unable to get the child from the graphbook, ie the scrolledWindow");
|
||||
if let Ok(graphview) = graphview.dynamic_cast::<GM::GraphView>() {
|
||||
let app = upgrade_weak!(app_weak);
|
||||
GPS_TRACE!("graphview.id() {} graphbook page {}", graphview.id(), page);
|
||||
|
@ -191,8 +192,11 @@ pub fn create_graphtab(app: &GPSApp, id: u32, name: Option<&str>) {
|
|||
.builder
|
||||
.object("graphbook")
|
||||
.expect("Couldn't get graphbook");
|
||||
let drawing_area_window: gtk::Viewport = gtk::Viewport::builder().build();
|
||||
drawing_area_window.set_child(Some(&*graphtab(app, id).graphview()));
|
||||
|
||||
let scrollwindow = gtk::ScrolledWindow::builder()
|
||||
.name("graphview_scroll")
|
||||
.child(&*graphtab(app, id).graphview())
|
||||
.build();
|
||||
|
||||
let tab_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||
let label = gt.widget_label();
|
||||
|
@ -209,8 +213,8 @@ pub fn create_graphtab(app: &GPSApp, id: u32, name: Option<&str>) {
|
|||
graphbook.remove_page(Some(current_graphtab(&app).id()));
|
||||
}));
|
||||
tab_box.append(&close_button);
|
||||
graphbook.append_page(&drawing_area_window, Some(&tab_box));
|
||||
graphbook.set_tab_reorderable(&drawing_area_window, true);
|
||||
graphbook.append_page(&scrollwindow, Some(&tab_box));
|
||||
graphbook.set_tab_reorderable(&scrollwindow, true);
|
||||
let app_weak = app.downgrade();
|
||||
gt.graphview().connect_local(
|
||||
"graph-updated",
|
||||
|
@ -221,7 +225,7 @@ pub fn create_graphtab(app: &GPSApp, id: u32, name: Option<&str>) {
|
|||
GPS_DEBUG!("Graph updated id={}", id);
|
||||
let _ = app
|
||||
.save_graph(
|
||||
Settings::default_graph_file_path()
|
||||
Settings::graph_file_path()
|
||||
.to_str()
|
||||
.expect("Unable to convert to string"),
|
||||
)
|
||||
|
@ -244,7 +248,6 @@ pub fn create_graphtab(app: &GPSApp, id: u32, name: Option<&str>) {
|
|||
node.set_tooltip_markup(description.as_deref());
|
||||
if !GPS::ElementInfo::element_factory_exists(&node.name()) {
|
||||
node.set_light(true);
|
||||
node.set_tooltip_markup(description.as_deref());
|
||||
}
|
||||
for port in node.all_ports(GM::PortDirection::All) {
|
||||
let caps = PropertyExt::property(&port,"_caps");
|
||||
|
|
|
@ -25,7 +25,7 @@ use once_cell::sync::Lazy;
|
|||
use std::io::Cursor;
|
||||
|
||||
use gtk::{
|
||||
gdk::{BUTTON_PRIMARY, BUTTON_SECONDARY},
|
||||
gdk,
|
||||
glib::{self, clone, subclass::Signal},
|
||||
graphene, gsk,
|
||||
prelude::*,
|
||||
|
@ -39,26 +39,38 @@ use std::{cmp::Ordering, collections::HashMap};
|
|||
static GRAPHVIEW_STYLE: &str = include_str!("graphview.css");
|
||||
pub static GRAPHVIEW_XML_VERSION: &str = "0.1";
|
||||
|
||||
const CANVAS_SIZE: f64 = 5000.0;
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
use log::warn;
|
||||
|
||||
pub struct DragState {
|
||||
node: glib::WeakRef<Node>,
|
||||
/// This stores the offset of the pointer to the origin of the node,
|
||||
/// so that we can keep the pointer over the same position when moving the node
|
||||
///
|
||||
/// The offset is normalized to the default zoom-level of 1.0.
|
||||
offset: graphene::Point,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GraphView {
|
||||
pub(super) id: Cell<u32>,
|
||||
pub(super) nodes: RefCell<HashMap<u32, Node>>,
|
||||
pub(super) nodes: RefCell<HashMap<u32, (Node, graphene::Point)>>,
|
||||
pub(super) links: RefCell<HashMap<u32, Link>>,
|
||||
pub(super) current_node_id: Cell<u32>,
|
||||
pub(super) current_port_id: Cell<u32>,
|
||||
pub(super) current_link_id: Cell<u32>,
|
||||
pub(super) port_selected: RefCell<Option<Port>>,
|
||||
pub(super) mouse_position: Cell<(f64, f64)>,
|
||||
pub dragged_node: RefCell<Option<DragState>>,
|
||||
pub hadjustment: RefCell<Option<gtk::Adjustment>>,
|
||||
pub vadjustment: RefCell<Option<gtk::Adjustment>>,
|
||||
pub zoom_factor: Cell<f64>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
@ -66,10 +78,11 @@ mod imp {
|
|||
const NAME: &'static str = "GraphView";
|
||||
type Type = super::GraphView;
|
||||
type ParentType = gtk::Widget;
|
||||
type Interfaces = (gtk::Scrollable,);
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
// The layout manager determines how child widgets are laid out.
|
||||
klass.set_layout_manager_type::<gtk::FixedLayout>();
|
||||
//klass.set_layout_manager_type::<gtk::FixedLayout>();
|
||||
klass.set_css_name("graphview");
|
||||
}
|
||||
}
|
||||
|
@ -79,65 +92,83 @@ mod imp {
|
|||
let obj = self.obj();
|
||||
self.parent_constructed();
|
||||
|
||||
let drag_state = Rc::new(RefCell::new(None));
|
||||
self.obj().set_overflow(gtk::Overflow::Hidden);
|
||||
|
||||
let drag_controller = gtk::GestureDrag::new();
|
||||
|
||||
drag_controller.connect_drag_begin(
|
||||
clone!(@strong drag_state => move |drag_controller, x, y| {
|
||||
let mut drag_state = drag_state.borrow_mut();
|
||||
let widget = drag_controller
|
||||
.widget()
|
||||
.dynamic_cast::<Self::Type>()
|
||||
.expect("drag-begin event is not on the GraphView");
|
||||
// pick() should at least return the widget itself.
|
||||
let target = widget.pick(x, y, gtk::PickFlags::DEFAULT).expect("drag-begin pick() did not return a widget");
|
||||
*drag_state = if target.ancestor(Port::static_type()).is_some() {
|
||||
// The user targeted a port, so the dragging should be handled by the Port
|
||||
// component instead of here.
|
||||
None
|
||||
} else if let Some(target) = target.ancestor(Node::static_type()) {
|
||||
// The user targeted a Node without targeting a specific Port.
|
||||
// Drag the Node around the screen.
|
||||
if let Some((x, y)) = widget.node_position(&target) {
|
||||
Some((target, x, y))
|
||||
} else {
|
||||
error!("Failed to obtain position of dragged node, drag aborted.");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
drag_controller.connect_drag_begin(|drag_controller, x, y| {
|
||||
let widget = drag_controller
|
||||
.widget()
|
||||
.dynamic_cast::<super::GraphView>()
|
||||
.expect("drag-begin event is not on the GraphView");
|
||||
let mut dragged_node = widget.imp().dragged_node.borrow_mut();
|
||||
|
||||
// pick() should at least return the widget itself.
|
||||
let target = widget
|
||||
.pick(x, y, gtk::PickFlags::DEFAULT)
|
||||
.expect("drag-begin pick() did not return a widget");
|
||||
*dragged_node = if target.ancestor(Port::static_type()).is_some() {
|
||||
// The user targeted a port, so the dragging should be handled by the Port
|
||||
// component instead of here.
|
||||
None
|
||||
} else if let Some(target) = target.ancestor(Node::static_type()) {
|
||||
// The user targeted a Node without targeting a specific Port.
|
||||
// Drag the Node around the screen.
|
||||
let node = target.dynamic_cast_ref::<Node>().unwrap();
|
||||
|
||||
let Some(canvas_node_pos) = widget.node_position(node) else {
|
||||
return;
|
||||
};
|
||||
let canvas_cursor_pos = widget
|
||||
.imp()
|
||||
.screen_space_to_canvas_space_transform()
|
||||
.transform_point(&graphene::Point::new(x as f32, y as f32));
|
||||
|
||||
Some(DragState {
|
||||
node: node.clone().downgrade(),
|
||||
offset: graphene::Point::new(
|
||||
canvas_cursor_pos.x() - canvas_node_pos.x(),
|
||||
canvas_cursor_pos.y() - canvas_node_pos.y(),
|
||||
),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
));
|
||||
drag_controller.connect_drag_update(
|
||||
clone!(@strong drag_state => move |drag_controller, x, y| {
|
||||
let widget = drag_controller
|
||||
.widget()
|
||||
.dynamic_cast::<Self::Type>()
|
||||
.expect("drag-update event is not on the GraphView");
|
||||
let drag_state = drag_state.borrow();
|
||||
if let Some((ref node, x1, y1)) = *drag_state {
|
||||
widget.move_node(node, x1 + x as f32, y1 + y as f32);
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
drag_controller.connect_drag_end(
|
||||
clone!(@strong drag_state => move |drag_controller, _x, _y| {
|
||||
let widget = drag_controller
|
||||
.widget()
|
||||
.dynamic_cast::<Self::Type>()
|
||||
.expect("drag-end event is not on the GraphView");
|
||||
widget.graph_updated();
|
||||
}
|
||||
),
|
||||
);
|
||||
});
|
||||
drag_controller.connect_drag_update(|drag_controller, x, y| {
|
||||
let widget = drag_controller
|
||||
.widget()
|
||||
.dynamic_cast::<super::GraphView>()
|
||||
.expect("drag-update event is not on the GraphView");
|
||||
let dragged_node = widget.imp().dragged_node.borrow();
|
||||
let Some(DragState { node, offset }) = dragged_node.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(node) = node.upgrade() else { return };
|
||||
|
||||
let (start_x, start_y) = drag_controller
|
||||
.start_point()
|
||||
.expect("Drag has no start point");
|
||||
|
||||
let onscreen_node_origin =
|
||||
graphene::Point::new((start_x + x) as f32, (start_y + y) as f32);
|
||||
let transform = widget.imp().screen_space_to_canvas_space_transform();
|
||||
let canvas_node_origin = transform.transform_point(&onscreen_node_origin);
|
||||
|
||||
widget.move_node(
|
||||
&node,
|
||||
&graphene::Point::new(
|
||||
canvas_node_origin.x() - offset.x(),
|
||||
canvas_node_origin.y() - offset.y(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
let gesture = gtk::GestureClick::new();
|
||||
gesture.set_button(0);
|
||||
gesture.connect_pressed(
|
||||
clone!(@weak obj, @weak drag_controller => move |gesture, _n_press, x, y| {
|
||||
if gesture.current_button() == BUTTON_SECONDARY {
|
||||
if gesture.current_button() == gdk::BUTTON_SECONDARY {
|
||||
let widget = drag_controller.widget()
|
||||
.dynamic_cast::<Self::Type>()
|
||||
.expect("click event is not on the GraphView");
|
||||
|
@ -155,7 +186,7 @@ mod imp {
|
|||
widget.unselect_all();
|
||||
obj.emit_by_name::<()>("graph-right-clicked", &[&graphene::Point::new(x as f32,y as f32)]);
|
||||
}
|
||||
} else if gesture.current_button() == BUTTON_PRIMARY {
|
||||
} else if gesture.current_button() == gdk::BUTTON_PRIMARY {
|
||||
let widget = drag_controller.widget()
|
||||
.dynamic_cast::<Self::Type>()
|
||||
.expect("click event is not on the GraphView");
|
||||
|
@ -177,7 +208,7 @@ mod imp {
|
|||
);
|
||||
|
||||
gesture.connect_released(clone!(@weak gesture, @weak obj, @weak drag_controller => move |_gesture, _n_press, x, y| {
|
||||
if gesture.current_button() == BUTTON_PRIMARY {
|
||||
if gesture.current_button() == gdk::BUTTON_PRIMARY {
|
||||
let widget = drag_controller
|
||||
.widget()
|
||||
.dynamic_cast::<Self::Type>()
|
||||
|
@ -229,6 +260,8 @@ mod imp {
|
|||
info!("double clicked link id {}", link.id());
|
||||
obj.emit_by_name::<()>("link-double-clicked", &[&link.id(), &graphene::Point::new(x as f32,y as f32)]);
|
||||
}
|
||||
} else {
|
||||
info!("double click {}",widget.width());
|
||||
}
|
||||
|
||||
// Click to something else than a port
|
||||
|
@ -237,27 +270,49 @@ mod imp {
|
|||
}
|
||||
}
|
||||
}));
|
||||
obj.add_controller(drag_controller);
|
||||
obj.add_controller(gesture);
|
||||
|
||||
let event_motion = gtk::EventControllerMotion::new();
|
||||
event_motion.connect_motion(glib::clone!(@weak obj => move |_e, x, y| {
|
||||
let graphview = obj;
|
||||
if graphview.selected_port().is_some() {
|
||||
graphview.set_mouse_position(x,y);
|
||||
graphview.queue_draw();
|
||||
graphview.queue_allocate();
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
obj.add_controller(drag_controller);
|
||||
obj.add_controller(gesture);
|
||||
obj.add_controller(event_motion);
|
||||
|
||||
let scroll_controller =
|
||||
gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::BOTH_AXES);
|
||||
|
||||
scroll_controller.connect_scroll(|eventcontroller, _, delta_y| {
|
||||
let event = eventcontroller.current_event().unwrap(); // We are inside the event handler, so it must have an event
|
||||
|
||||
if event
|
||||
.modifier_state()
|
||||
.contains(gdk::ModifierType::CONTROL_MASK)
|
||||
{
|
||||
let widget = eventcontroller
|
||||
.widget()
|
||||
.downcast::<super::GraphView>()
|
||||
.unwrap();
|
||||
widget.set_zoom_factor(widget.zoom_factor() + (0.1 * -delta_y), None);
|
||||
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
self.obj().add_controller(scroll_controller);
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.nodes
|
||||
.borrow()
|
||||
.values()
|
||||
.for_each(|node| node.unparent())
|
||||
.for_each(|(node, _)| node.unparent())
|
||||
}
|
||||
|
||||
fn signals() -> &'static [Signal] {
|
||||
|
@ -301,18 +356,97 @@ mod imp {
|
|||
});
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hadjustment"),
|
||||
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vadjustment"),
|
||||
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hscroll-policy"),
|
||||
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vscroll-policy"),
|
||||
glib::ParamSpecDouble::builder("zoom-factor")
|
||||
.minimum(0.3)
|
||||
.maximum(4.0)
|
||||
.default_value(1.0)
|
||||
.flags(glib::ParamFlags::CONSTRUCT | glib::ParamFlags::READWRITE)
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"hadjustment" => self.hadjustment.borrow().to_value(),
|
||||
"vadjustment" => self.vadjustment.borrow().to_value(),
|
||||
"hscroll-policy" | "vscroll-policy" => gtk::ScrollablePolicy::Natural.to_value(),
|
||||
"zoom-factor" => self.zoom_factor.get().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
let obj = self.obj();
|
||||
|
||||
match pspec.name() {
|
||||
"hadjustment" => {
|
||||
obj.set_adjustment(&obj, value.get().ok(), gtk::Orientation::Horizontal)
|
||||
}
|
||||
"vadjustment" => {
|
||||
obj.set_adjustment(&obj, value.get().ok(), gtk::Orientation::Vertical)
|
||||
}
|
||||
"hscroll-policy" | "vscroll-policy" => {}
|
||||
"zoom-factor" => {
|
||||
self.zoom_factor.set(value.get().unwrap());
|
||||
obj.queue_allocate();
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for GraphView {
|
||||
fn size_allocate(&self, _width: i32, _height: i32, baseline: i32) {
|
||||
let widget = &*self.obj();
|
||||
|
||||
let zoom_factor = self.zoom_factor.get();
|
||||
|
||||
for (node, point) in self.nodes.borrow().values() {
|
||||
let (_, natural_size) = node.preferred_size();
|
||||
|
||||
let transform = self
|
||||
.canvas_space_to_screen_space_transform()
|
||||
.translate(point);
|
||||
|
||||
node.allocate(
|
||||
(natural_size.width() as f64 * zoom_factor).ceil() as i32,
|
||||
(natural_size.height() as f64 * zoom_factor).ceil() as i32,
|
||||
baseline,
|
||||
Some(transform),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref hadjustment) = *self.hadjustment.borrow() {
|
||||
widget.set_adjustment_values(widget, hadjustment, gtk::Orientation::Horizontal);
|
||||
}
|
||||
if let Some(ref vadjustment) = *self.vadjustment.borrow() {
|
||||
widget.set_adjustment_values(widget, vadjustment, gtk::Orientation::Vertical);
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(&self, snapshot: >k::Snapshot) {
|
||||
/* FIXME: A lot of hardcoded values in here.
|
||||
Try to use relative units (em) and colours from the theme as much as possible. */
|
||||
|
||||
let widget = &*self.obj();
|
||||
let alloc = widget.allocation();
|
||||
// Draw all children
|
||||
// Draw all visible children
|
||||
self.nodes
|
||||
.borrow()
|
||||
.values()
|
||||
.for_each(|node| self.obj().snapshot_child(node, snapshot));
|
||||
// Cull nodes from rendering when they are outside the visible canvas area
|
||||
.filter(|(node, _)| alloc.intersect(&node.allocation()).is_some())
|
||||
.for_each(|(node, _)| widget.snapshot_child(node, snapshot));
|
||||
|
||||
for link in self.links.borrow().values() {
|
||||
if let Some((from_x, from_y, to_x, to_y)) = self.link_coordinates(link) {
|
||||
|
@ -353,56 +487,77 @@ mod imp {
|
|||
}
|
||||
}
|
||||
|
||||
impl ScrollableImpl for GraphView {}
|
||||
|
||||
impl GraphView {
|
||||
/// Returns a [`gsk::Transform`] matrix that can translate from canvas space to screen space.
|
||||
///
|
||||
/// Canvas space is non-zoomed, and (0, 0) is fixed at the middle of the graph. \
|
||||
/// Screen space is zoomed and adjusted for scrolling, (0, 0) is at the top-left corner of the window.
|
||||
///
|
||||
/// This is the inverted form of [`Self::screen_space_to_canvas_space_transform()`].
|
||||
fn canvas_space_to_screen_space_transform(&self) -> gsk::Transform {
|
||||
let hadj = self.hadjustment.borrow().as_ref().unwrap().value();
|
||||
let vadj = self.vadjustment.borrow().as_ref().unwrap().value();
|
||||
let zoom_factor = self.zoom_factor.get();
|
||||
|
||||
gsk::Transform::new()
|
||||
.translate(&graphene::Point::new(-hadj as f32, -vadj as f32))
|
||||
.scale(zoom_factor as f32, zoom_factor as f32)
|
||||
}
|
||||
|
||||
/// Returns a [`gsk::Transform`] matrix that can translate from screen space to canvas space.
|
||||
///
|
||||
/// This is the inverted form of [`Self::canvas_space_to_screen_space_transform()`], see that function for a more detailed explanation.
|
||||
fn screen_space_to_canvas_space_transform(&self) -> gsk::Transform {
|
||||
self.canvas_space_to_screen_space_transform()
|
||||
.invert()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn link_from_coordinates(&self, node_from: u32, port_from: u32) -> (f64, f64) {
|
||||
let nodes = self.nodes.borrow();
|
||||
|
||||
let widget = &*self.obj();
|
||||
let from_node = nodes
|
||||
.get(&node_from)
|
||||
.unwrap_or_else(|| (panic!("Unable to get node from {}", node_from)));
|
||||
|
||||
let from_port = from_node
|
||||
.0
|
||||
.port(port_from)
|
||||
.unwrap_or_else(|| panic!("Unable to get port from {}", port_from));
|
||||
let (mut from_x, mut from_y, fw, fh) = (
|
||||
from_port.allocation().x(),
|
||||
from_port.allocation().y(),
|
||||
from_port.allocation().width(),
|
||||
from_port.allocation().height(),
|
||||
);
|
||||
let (fnx, fny) = (from_node.allocation().x(), from_node.allocation().y());
|
||||
|
||||
if let Some((port_x, port_y)) = from_port.translate_coordinates(from_node, 0.0, 0.0) {
|
||||
from_x = fnx + fw + port_x as i32;
|
||||
from_y = fny + (fh / 2) + port_y as i32;
|
||||
}
|
||||
let (x, y) = from_port
|
||||
.translate_coordinates(
|
||||
widget,
|
||||
(from_port.width() / 2) as f64,
|
||||
(from_port.height() / 2) as f64,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(from_x as f64, from_y as f64)
|
||||
(x, y)
|
||||
}
|
||||
|
||||
fn link_to_coordinates(&self, node_to: u32, port_to: u32) -> (f64, f64) {
|
||||
let nodes = self.nodes.borrow();
|
||||
let widget = &*self.obj();
|
||||
|
||||
let to_node = nodes
|
||||
.get(&node_to)
|
||||
.unwrap_or_else(|| panic!("Unable to get node to {}", node_to));
|
||||
let to_port = to_node
|
||||
.0
|
||||
.port(port_to)
|
||||
.unwrap_or_else(|| panic!("Unable to get port to {}", port_to));
|
||||
let (mut to_x, mut to_y, th) = (
|
||||
to_port.allocation().x(),
|
||||
to_port.allocation().y(),
|
||||
to_port.allocation().height(),
|
||||
);
|
||||
let (x, y) = to_port
|
||||
.translate_coordinates(
|
||||
widget,
|
||||
(to_port.width() / 2) as f64,
|
||||
(to_port.height() / 2) as f64,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (tnx, tny) = (to_node.allocation().x(), to_node.allocation().y());
|
||||
|
||||
if let Some((port_x, port_y)) = to_port.translate_coordinates(to_node, 0.0, 0.0) {
|
||||
to_x += tnx + port_x as i32;
|
||||
to_y = tny + (th / 2) + port_y as i32;
|
||||
}
|
||||
//trace!("{} {} -> {} {}", fx, fy, tx, ty);
|
||||
(to_x.into(), to_y.into())
|
||||
(x, y)
|
||||
}
|
||||
/// Retrieves coordinates for the drawn link to start at and to end at.
|
||||
///
|
||||
|
@ -469,6 +624,8 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl GraphView {
|
||||
pub const ZOOM_MIN: f64 = 0.3;
|
||||
pub const ZOOM_MAX: f64 = 4.0;
|
||||
/// Create a new graphview
|
||||
///
|
||||
/// # Returns
|
||||
|
@ -482,6 +639,7 @@ impl GraphView {
|
|||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
|
||||
glib::Object::new::<Self>()
|
||||
}
|
||||
|
||||
|
@ -492,7 +650,7 @@ impl GraphView {
|
|||
private.id.set(id)
|
||||
}
|
||||
|
||||
/// Retrives the graphview id
|
||||
/// Retrieves the graphview id
|
||||
///
|
||||
pub fn id(&self) -> u32 {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
|
@ -505,6 +663,40 @@ impl GraphView {
|
|||
self.remove_all_nodes();
|
||||
}
|
||||
|
||||
pub fn zoom_factor(&self) -> f64 {
|
||||
self.property("zoom-factor")
|
||||
}
|
||||
|
||||
pub fn set_zoom_factor(&self, zoom_factor: f64, anchor: Option<(f64, f64)>) {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
let zoom_factor = zoom_factor.clamp(Self::ZOOM_MIN, Self::ZOOM_MAX);
|
||||
|
||||
let (anchor_x_screen, anchor_y_screen) = anchor.unwrap_or_else(|| {
|
||||
(
|
||||
self.allocation().width() as f64 / 2.0,
|
||||
self.allocation().height() as f64 / 2.0,
|
||||
)
|
||||
});
|
||||
|
||||
let old_zoom = private.zoom_factor.get();
|
||||
let hadjustment_ref = private.hadjustment.borrow();
|
||||
let vadjustment_ref = private.vadjustment.borrow();
|
||||
let hadjustment = hadjustment_ref.as_ref().unwrap();
|
||||
let vadjustment = vadjustment_ref.as_ref().unwrap();
|
||||
|
||||
let x_total = (anchor_x_screen + hadjustment.value()) / old_zoom;
|
||||
let y_total = (anchor_y_screen + vadjustment.value()) / old_zoom;
|
||||
|
||||
let new_hadjustment = x_total * zoom_factor - anchor_x_screen;
|
||||
let new_vadjustment = y_total * zoom_factor - anchor_y_screen;
|
||||
|
||||
hadjustment.set_value(new_hadjustment);
|
||||
vadjustment.set_value(new_vadjustment);
|
||||
|
||||
self.set_property("zoom-factor", zoom_factor);
|
||||
info!("zoom factor {}", zoom_factor);
|
||||
}
|
||||
|
||||
// Node
|
||||
|
||||
/// Create a new node with a new id
|
||||
|
@ -544,7 +736,7 @@ impl GraphView {
|
|||
let private = imp::GraphView::from_obj(self);
|
||||
node.set_parent(self);
|
||||
|
||||
// Place widgets in colums of 3, growing down
|
||||
// Place widgets in columns of 3, growing down
|
||||
let x = if let Some(node_type) = node.node_type() {
|
||||
match node_type {
|
||||
NodeType::Source => 20.0,
|
||||
|
@ -560,9 +752,10 @@ impl GraphView {
|
|||
.nodes
|
||||
.borrow()
|
||||
.values()
|
||||
.filter_map(|node| {
|
||||
// Map nodes to locations, discard nodes without location
|
||||
self.node_position(&node.clone().upcast())
|
||||
.map(|node| {
|
||||
// Map nodes to their locations
|
||||
let point = self.node_position(&node.0.clone().upcast()).unwrap();
|
||||
(point.x(), point.y())
|
||||
})
|
||||
.filter(|(x2, _)| {
|
||||
// Only look for other nodes that have a similar x coordinate
|
||||
|
@ -572,11 +765,13 @@ impl GraphView {
|
|||
// Get max in column
|
||||
y1.partial_cmp(y2).unwrap_or(Ordering::Equal)
|
||||
})
|
||||
.map_or(20_f32, |(_x, y)| y + 100.0);
|
||||
.map_or(20_f32, |(_x, y)| y + 120.0);
|
||||
|
||||
self.move_node(&node.clone().upcast(), x, y);
|
||||
let node_id = node.id();
|
||||
private.nodes.borrow_mut().insert(node.id(), node);
|
||||
private
|
||||
.nodes
|
||||
.borrow_mut()
|
||||
.insert(node.id(), (node, graphene::Point::new(x, y)));
|
||||
self.emit_by_name::<()>("node-added", &[&private.id.get(), &node_id]);
|
||||
self.graph_updated();
|
||||
}
|
||||
|
@ -585,17 +780,16 @@ impl GraphView {
|
|||
///
|
||||
pub fn remove_node(&self, id: u32) {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
let mut nodes = private.nodes.borrow_mut();
|
||||
if let Some(node) = nodes.remove(&id) {
|
||||
while let Some(link_id) = self.node_is_linked(node.id()) {
|
||||
|
||||
if let Some(node) = private.nodes.borrow_mut().remove(&id) {
|
||||
while let Some(link_id) = self.node_is_linked(node.0.id()) {
|
||||
info!("Remove link id {}", link_id);
|
||||
private.links.borrow_mut().remove(&link_id);
|
||||
}
|
||||
node.unparent();
|
||||
node.0.unparent();
|
||||
} else {
|
||||
warn!("Tried to remove non-existant node (id={}) from graph", id);
|
||||
warn!("Tried to remove non-existent node (id={}) from graph", id);
|
||||
}
|
||||
self.queue_draw();
|
||||
}
|
||||
|
||||
/// Select all nodes according to the NodeType
|
||||
|
@ -607,9 +801,9 @@ impl GraphView {
|
|||
let nodes_list: Vec<_> = nodes
|
||||
.iter()
|
||||
.filter(|(_, node)| {
|
||||
*node.node_type().unwrap() == node_type || node_type == NodeType::All
|
||||
*node.0.node_type().unwrap() == node_type || node_type == NodeType::All
|
||||
})
|
||||
.map(|(_, node)| node.clone())
|
||||
.map(|(_, node)| node.0.clone())
|
||||
.collect();
|
||||
nodes_list
|
||||
}
|
||||
|
@ -619,7 +813,12 @@ impl GraphView {
|
|||
/// Returns `None` if the node is not in the graphview.
|
||||
pub fn node(&self, id: u32) -> Option<Node> {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
private.nodes.borrow().get(&id).cloned()
|
||||
|
||||
if let Some(node) = private.nodes.borrow().get(&id).cloned() {
|
||||
Some(node.0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the node with the specified node name inside the graphview.
|
||||
|
@ -628,8 +827,8 @@ impl GraphView {
|
|||
pub fn node_by_unique_name(&self, unique_name: &str) -> Option<Node> {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
for node in private.nodes.borrow().values() {
|
||||
if node.unique_name() == unique_name {
|
||||
return Some(node.clone());
|
||||
if node.0.unique_name() == unique_name {
|
||||
return Some(node.0.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
|
@ -646,7 +845,6 @@ impl GraphView {
|
|||
private.current_node_id.set(0);
|
||||
private.current_port_id.set(0);
|
||||
private.current_link_id.set(0);
|
||||
self.queue_draw();
|
||||
}
|
||||
|
||||
/// Check if the node is linked
|
||||
|
@ -665,21 +863,12 @@ impl GraphView {
|
|||
/// Get the position of the specified node inside the graphview.
|
||||
///
|
||||
/// Returns `None` if the node is not in the graphview.
|
||||
pub(super) fn node_position(&self, node: >k::Widget) -> Option<(f32, f32)> {
|
||||
let layout_manager = self
|
||||
.layout_manager()
|
||||
.expect("Failed to get layout manager")
|
||||
.dynamic_cast::<gtk::FixedLayout>()
|
||||
.expect("Failed to cast to FixedLayout");
|
||||
|
||||
let node = layout_manager
|
||||
.layout_child(node)
|
||||
.dynamic_cast::<gtk::FixedLayoutChild>()
|
||||
.expect("Could not cast to FixedLayoutChild");
|
||||
let transform = node
|
||||
.transform()
|
||||
.expect("Failed to obtain transform from layout child");
|
||||
Some(transform.to_translate())
|
||||
pub(super) fn node_position(&self, node: &Node) -> Option<graphene::Point> {
|
||||
self.imp()
|
||||
.nodes
|
||||
.borrow()
|
||||
.get(&node.id())
|
||||
.map(|(_, point)| *point)
|
||||
}
|
||||
|
||||
// Port
|
||||
|
@ -713,9 +902,8 @@ impl GraphView {
|
|||
/// Return true if the port presence is not always.
|
||||
pub fn can_remove_port(&self, node_id: u32, port_id: u32) -> bool {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
let nodes = private.nodes.borrow();
|
||||
if let Some(node) = nodes.get(&node_id) {
|
||||
return node.can_remove_port(port_id);
|
||||
if let Some(node) = private.nodes.borrow().get(&node_id) {
|
||||
return node.0.can_remove_port(port_id);
|
||||
}
|
||||
warn!("Unable to find a node with the id {}", node_id);
|
||||
false
|
||||
|
@ -725,12 +913,11 @@ impl GraphView {
|
|||
///
|
||||
pub fn remove_port(&self, node_id: u32, port_id: u32) {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
let nodes = private.nodes.borrow();
|
||||
if let Some(node) = nodes.get(&node_id) {
|
||||
if let Some(node) = private.nodes.borrow().get(&node_id) {
|
||||
if let Some(link_id) = self.port_is_linked(port_id) {
|
||||
self.remove_link(link_id);
|
||||
}
|
||||
node.remove_port(port_id);
|
||||
node.0.remove_port(port_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -864,8 +1051,8 @@ impl GraphView {
|
|||
}
|
||||
}
|
||||
for node in private.nodes.borrow_mut().values() {
|
||||
if node.selected() {
|
||||
node_id = Some(node.id());
|
||||
if node.0.selected() {
|
||||
node_id = Some(node.0.id());
|
||||
}
|
||||
}
|
||||
if let Some(id) = link_id {
|
||||
|
@ -895,8 +1082,8 @@ impl GraphView {
|
|||
)?;
|
||||
|
||||
//Get the nodes
|
||||
let nodes = self.all_nodes(NodeType::All);
|
||||
for node in nodes {
|
||||
|
||||
for node in self.all_nodes(NodeType::All) {
|
||||
writer.write(
|
||||
XMLWEvent::start_element("Node")
|
||||
.attr("name", &node.name())
|
||||
|
@ -1104,12 +1291,13 @@ impl GraphView {
|
|||
"Node" => {
|
||||
if let Some(node) = current_node {
|
||||
let id = node.id();
|
||||
let position = node.position();
|
||||
let position =
|
||||
graphene::Point::new(node.position().0, node.position().1);
|
||||
node.update_properties(¤t_node_properties);
|
||||
current_node_properties.clear();
|
||||
self.add_node(node);
|
||||
if let Some(node) = self.node(id) {
|
||||
self.move_node(&node.upcast(), position.0, position.1);
|
||||
self.move_node(&node, &position);
|
||||
}
|
||||
|
||||
self.update_current_node_id(id);
|
||||
|
@ -1207,38 +1395,31 @@ impl GraphView {
|
|||
false
|
||||
}
|
||||
|
||||
fn move_node(&self, widget: >k::Widget, x: f32, y: f32) {
|
||||
let node = widget
|
||||
.clone()
|
||||
.dynamic_cast::<Node>()
|
||||
.expect("Unable to convert to Node");
|
||||
node.set_position(x, y);
|
||||
let layout_manager = self
|
||||
.layout_manager()
|
||||
.expect("Failed to get layout manager")
|
||||
.dynamic_cast::<gtk::FixedLayout>()
|
||||
.expect("Failed to cast to FixedLayout");
|
||||
fn move_node(&self, widget: &Node, point: &graphene::Point) {
|
||||
let mut nodes = self.imp().nodes.borrow_mut();
|
||||
let node = nodes
|
||||
.get_mut(&widget.id())
|
||||
.expect("Node is not on the graph");
|
||||
node.1 = graphene::Point::new(
|
||||
point.x().clamp(
|
||||
-(CANVAS_SIZE / 2.0) as f32,
|
||||
(CANVAS_SIZE / 2.0) as f32 - widget.width() as f32,
|
||||
),
|
||||
point.y().clamp(
|
||||
-(CANVAS_SIZE / 2.0) as f32,
|
||||
(CANVAS_SIZE / 2.0) as f32 - widget.height() as f32,
|
||||
),
|
||||
);
|
||||
|
||||
let transform = gsk::Transform::new()
|
||||
// Nodes should not be able to be dragged out of the view, so we use `max(coordinate, 0.0)` to prevent that.
|
||||
.translate(&graphene::Point::new(f32::max(x, 0.0), f32::max(y, 0.0)));
|
||||
|
||||
layout_manager
|
||||
.layout_child(widget)
|
||||
.dynamic_cast::<gtk::FixedLayoutChild>()
|
||||
.expect("Could not cast to FixedLayoutChild")
|
||||
.set_transform(&transform);
|
||||
|
||||
// FIXME: If links become proper widgets,
|
||||
// we don't need to redraw the full graph everytime.
|
||||
self.queue_draw();
|
||||
self.queue_allocate();
|
||||
}
|
||||
|
||||
fn unselect_nodes(&self) {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
for node in private.nodes.borrow_mut().values() {
|
||||
node.set_selected(false);
|
||||
node.unselect_all_ports();
|
||||
node.0.set_selected(false);
|
||||
node.0.unselect_all_ports();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1286,7 +1467,7 @@ impl GraphView {
|
|||
|
||||
fn graph_updated(&self) {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
self.queue_draw();
|
||||
self.queue_allocate();
|
||||
self.emit_by_name::<()>("graph-updated", &[&private.id.get()]);
|
||||
}
|
||||
|
||||
|
@ -1362,6 +1543,47 @@ impl GraphView {
|
|||
private.current_port_id.set(port_id);
|
||||
}
|
||||
}
|
||||
fn set_adjustment(
|
||||
&self,
|
||||
obj: &super::GraphView,
|
||||
adjustment: Option<>k::Adjustment>,
|
||||
orientation: gtk::Orientation,
|
||||
) {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
match orientation {
|
||||
gtk::Orientation::Horizontal => *private.hadjustment.borrow_mut() = adjustment.cloned(),
|
||||
gtk::Orientation::Vertical => *private.vadjustment.borrow_mut() = adjustment.cloned(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
|
||||
if let Some(adjustment) = adjustment {
|
||||
adjustment.connect_value_changed(clone!(@weak obj => move |_| obj.queue_allocate() ));
|
||||
}
|
||||
}
|
||||
|
||||
fn set_adjustment_values(
|
||||
&self,
|
||||
obj: &super::GraphView,
|
||||
adjustment: >k::Adjustment,
|
||||
orientation: gtk::Orientation,
|
||||
) {
|
||||
let private = imp::GraphView::from_obj(self);
|
||||
let size = match orientation {
|
||||
gtk::Orientation::Horizontal => obj.width(),
|
||||
gtk::Orientation::Vertical => obj.height(),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
let zoom_factor = private.zoom_factor.get();
|
||||
|
||||
adjustment.configure(
|
||||
adjustment.value(),
|
||||
-(CANVAS_SIZE / 2.0) * zoom_factor,
|
||||
(CANVAS_SIZE / 2.0) * zoom_factor,
|
||||
(f64::from(size) * 0.1) * zoom_factor,
|
||||
(f64::from(size) * 0.9) * zoom_factor,
|
||||
f64::from(size) * zoom_factor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GraphView {
|
||||
|
|
|
@ -6,7 +6,6 @@ mod property;
|
|||
mod selection;
|
||||
|
||||
pub use graphview::GraphView;
|
||||
pub use link::Link;
|
||||
pub use node::Node;
|
||||
pub use node::NodeType;
|
||||
pub use port::Port;
|
||||
|
|
|
@ -48,7 +48,7 @@ impl PortDirection {
|
|||
/// Port's presence
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Copy)]
|
||||
pub enum PortPresence {
|
||||
/// Can not be removed from his parent independantly
|
||||
/// Can not be removed from his parent independently
|
||||
Always,
|
||||
/// Can be removed from a node
|
||||
Sometimes,
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use gtk::glib::Sender;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use simplelog::*;
|
||||
use std::fmt;
|
||||
|
@ -14,6 +13,13 @@ use std::io;
|
|||
|
||||
use std::fs::File;
|
||||
|
||||
use chrono::Local;
|
||||
use std::sync::Mutex;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref MSG_LOGGER: Mutex<Option<MessageLogger>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
|
||||
pub enum LogLevel {
|
||||
|
@ -25,6 +31,13 @@ pub enum LogLevel {
|
|||
Trace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LogType {
|
||||
App,
|
||||
Gst,
|
||||
Message,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
pub fn from_u32(value: u32) -> LogLevel {
|
||||
match value {
|
||||
|
@ -49,7 +62,7 @@ impl fmt::Display for LogLevel {
|
|||
macro_rules! GPS_ERROR (
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ({
|
||||
logger::print_log(logger::LogLevel::Error, format_args!($($arg)*).to_string());
|
||||
logger::print_log(logger::LogLevel::Error, format!("{}\t{}",glib::function_name!(),format_args!($($arg)*).to_string()));
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -57,7 +70,7 @@ macro_rules! GPS_ERROR (
|
|||
macro_rules! GPS_WARN (
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ({
|
||||
logger::print_log(logger::LogLevel::Warning, format_args!($($arg)*).to_string());
|
||||
logger::print_log(logger::LogLevel::Warning, format!("{}\t{}",glib::function_name!(),format_args!($($arg)*).to_string()));
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -65,7 +78,7 @@ macro_rules! GPS_WARN (
|
|||
macro_rules! GPS_INFO (
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ({
|
||||
logger::print_log(logger::LogLevel::Info, format_args!($($arg)*).to_string());
|
||||
logger::print_log(logger::LogLevel::Info, format!("{}\t{}",glib::function_name!(),format_args!($($arg)*).to_string()));
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -73,7 +86,23 @@ macro_rules! GPS_INFO (
|
|||
macro_rules! GPS_DEBUG (
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ({
|
||||
logger::print_log(logger::LogLevel::Debug, format_args!($($arg)*).to_string());
|
||||
logger::print_log(logger::LogLevel::Debug, format!("{}\t{}",glib::function_name!(),format_args!($($arg)*).to_string()));
|
||||
})
|
||||
);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! GPS_MSG_LOG (
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ({
|
||||
logger::print_msg_logger(logger::LogType::Message, format_args!($($arg)*).to_string());
|
||||
})
|
||||
);
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! GPS_GST_LOG (
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ({
|
||||
logger::print_msg_logger(logger::LogType::Gst, format_args!($($arg)*).to_string());
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -86,7 +115,7 @@ macro_rules! GPS_TRACE (
|
|||
);
|
||||
|
||||
struct WriteAdapter {
|
||||
sender: Sender<String>,
|
||||
sender: async_channel::Sender<(LogType, String)>,
|
||||
buffer: String,
|
||||
}
|
||||
|
||||
|
@ -97,8 +126,8 @@ impl io::Write for WriteAdapter {
|
|||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||
if self.buffer.ends_with('\n') {
|
||||
self.buffer.pop();
|
||||
self.sender.send(self.buffer.clone()).unwrap();
|
||||
self.buffer = String::from("");
|
||||
let _ = self.sender.try_send((LogType::App, self.buffer.clone()));
|
||||
self.buffer.clear();
|
||||
}
|
||||
|
||||
Ok(buf.len())
|
||||
|
@ -120,7 +149,7 @@ fn translate_to_simple_logger(log_level: LogLevel) -> LevelFilter {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn init_logger(sender: Sender<String>, log_file: &str) {
|
||||
pub fn init_logger(sender: async_channel::Sender<(LogType, String)>, log_file: &str) {
|
||||
simplelog::CombinedLogger::init(vec![
|
||||
WriteLogger::new(
|
||||
translate_to_simple_logger(LogLevel::Trace),
|
||||
|
@ -169,3 +198,36 @@ pub fn print_log(log_level: LogLevel, msg: String) {
|
|||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessageLogger {
|
||||
sender: async_channel::Sender<(LogType, String)>,
|
||||
}
|
||||
|
||||
impl MessageLogger {
|
||||
pub fn new(sender: async_channel::Sender<(LogType, String)>) -> Self {
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
pub fn print_log(&self, log_type: LogType, msg: String) {
|
||||
let to_send = format!("{}\t{}", Local::now().format("%H:%M:%S"), msg);
|
||||
self.sender
|
||||
.try_send((log_type, to_send))
|
||||
.expect("Unable to send the log");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_msg_logger(sender: async_channel::Sender<(LogType, String)>) {
|
||||
let mut msg_logger = MSG_LOGGER.lock().unwrap();
|
||||
if msg_logger.is_none() {
|
||||
// Initialize the variable
|
||||
*msg_logger = Some(MessageLogger::new(sender));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_msg_logger(log_type: LogType, msg: String) {
|
||||
let msg_logger = MSG_LOGGER.lock().unwrap();
|
||||
if let Some(logger) = msg_logger.as_ref() {
|
||||
logger.print_log(log_type, msg);
|
||||
}
|
||||
}
|
||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -22,14 +22,29 @@ use gtk::prelude::*;
|
|||
|
||||
use crate::app::GPSApp;
|
||||
use crate::common::init;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct Command {
|
||||
#[structopt(about = "Sets the pipeline description", default_value = "")]
|
||||
pipeline: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// gio::resources_register_include!("compiled.gresource").unwrap();
|
||||
init().expect("Unable to init app");
|
||||
let application = gtk::Application::new(Some(config::APP_ID), Default::default());
|
||||
let application = gtk::Application::new(
|
||||
Some(config::APP_ID),
|
||||
gtk::gio::ApplicationFlags::HANDLES_COMMAND_LINE,
|
||||
);
|
||||
application.connect_startup(|application| {
|
||||
GPSApp::on_startup(application);
|
||||
let args = Command::from_args();
|
||||
GPSApp::on_startup(application, &args.pipeline);
|
||||
});
|
||||
|
||||
application.connect_command_line(|_app, _cmd_line| {
|
||||
// structopt already handled arguments
|
||||
0
|
||||
});
|
||||
application.run();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
conf = configuration_data()
|
||||
conf.set_quoted('APP_ID', application_id)
|
||||
conf.set_quoted('VERSION', version + version_suffix)
|
||||
|
||||
configure_file(
|
||||
input: 'config.rs.in',
|
||||
|
@ -9,7 +8,7 @@ configure_file(
|
|||
)
|
||||
|
||||
# Copy the config.rs output to the source directory.
|
||||
run_command(python3, '-c', 'import shutil; shutil.copyfile("@0@", "@1@")'.format(join_paths(meson.build_root(), 'src', 'config.rs'), join_paths(meson.source_root(), 'src', 'config.rs')))
|
||||
run_command(python3, '-c', 'import shutil; shutil.copyfile("@0@", "@1@")'.format(join_paths(meson.project_build_root(), 'src', 'config.rs'), join_paths(meson.project_source_root(), 'src', 'config.rs')))
|
||||
|
||||
rust_sources = files(
|
||||
'gps/player.rs',
|
||||
|
@ -38,7 +37,7 @@ rust_sources = files(
|
|||
|
||||
sources = [cargo_sources, rust_sources]
|
||||
|
||||
cargo_script = find_program(join_paths(meson.source_root(), 'build-aux/cargo.py'))
|
||||
cargo_script = find_program(join_paths(meson.project_source_root(), 'build-aux/cargo.py'))
|
||||
|
||||
app_name = meson.project_name()
|
||||
if host_system == 'windows'
|
||||
|
@ -55,8 +54,8 @@ cargo_release = custom_target(
|
|||
install_dir: get_option('bindir'),
|
||||
command: [
|
||||
cargo_script,
|
||||
meson.build_root(),
|
||||
meson.source_root(),
|
||||
meson.project_build_root(),
|
||||
meson.project_source_root(),
|
||||
'@OUTPUT@',
|
||||
get_option('buildtype'),
|
||||
app_name,
|
||||
|
|
|
@ -31,45 +31,55 @@ pub struct Settings {
|
|||
}
|
||||
|
||||
impl Settings {
|
||||
fn settings_file_exist() {
|
||||
let s = Settings::settings_file_path();
|
||||
|
||||
fn create_path_if_not(s: &PathBuf) {
|
||||
if !s.exists() {
|
||||
if let Some(parent_dir) = s.parent() {
|
||||
if !parent_dir.exists() {
|
||||
if let Err(e) = create_dir_all(parent_dir) {
|
||||
GPS_ERROR!(
|
||||
"Error while trying to build settings snapshot_directory '{}': {}",
|
||||
parent_dir.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Err(e) = create_dir_all(s) {
|
||||
GPS_ERROR!(
|
||||
"Error while trying to build settings snapshot_directory '{}': {}",
|
||||
s.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn settings_file_path() -> PathBuf {
|
||||
fn default_app_folder() -> PathBuf {
|
||||
let mut path = glib::user_config_dir();
|
||||
path.push(config::APP_ID);
|
||||
path
|
||||
}
|
||||
|
||||
fn settings_file_path() -> PathBuf {
|
||||
let mut path = Settings::default_app_folder();
|
||||
Settings::create_path_if_not(&path);
|
||||
path.push("settings.toml");
|
||||
path
|
||||
}
|
||||
// Public methods
|
||||
pub fn default_graph_file_path() -> PathBuf {
|
||||
let mut path = glib::user_config_dir();
|
||||
path.push(config::APP_ID);
|
||||
pub fn graph_file_path() -> PathBuf {
|
||||
let mut path = Settings::default_app_folder();
|
||||
Settings::create_path_if_not(&path);
|
||||
path.push("default_graph.toml");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn default_log_file_path() -> PathBuf {
|
||||
let mut path = glib::user_config_dir();
|
||||
path.push(config::APP_ID);
|
||||
pub fn log_file_path() -> PathBuf {
|
||||
let mut path = Settings::default_app_folder();
|
||||
Settings::create_path_if_not(&path);
|
||||
path.push("gstpipelinestudio.log");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn gst_log_level() -> String {
|
||||
let settings = Settings::load_settings();
|
||||
let binding = "0".to_string();
|
||||
let level = settings
|
||||
.preferences
|
||||
.get("gst_log_level")
|
||||
.unwrap_or(&binding);
|
||||
level.clone()
|
||||
}
|
||||
|
||||
pub fn set_recent_pipeline_description(pipeline: &str) {
|
||||
let mut settings = Settings::load_settings();
|
||||
settings.recent_pipeline = pipeline.to_string();
|
||||
|
@ -105,7 +115,6 @@ impl Settings {
|
|||
|
||||
// Save the provided settings to the settings path
|
||||
pub fn save_settings(settings: &Settings) {
|
||||
Settings::settings_file_exist();
|
||||
let s = Settings::settings_file_path();
|
||||
if let Err(e) = serde_any::to_file(&s, settings) {
|
||||
GPS_ERROR!("Error while trying to save file: {} {}", s.display(), e);
|
||||
|
|
|
@ -209,24 +209,63 @@
|
|||
<property name="orientation">vertical</property>
|
||||
<property name="position">400</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="drawing_area-window">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="child">
|
||||
<object class="GtkNotebook" id="graphbook">
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="child">
|
||||
<object class="GtkTreeView" id="treeview-logger"/>
|
||||
</property>
|
||||
</object>
|
||||
<object class="GtkNotebook" id="notebook-debug">
|
||||
<child>
|
||||
<object class="GtkNotebookPage">
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="child">
|
||||
<object class="GtkTreeView" id="treeview-app-logger"/>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="tab">
|
||||
<object class="GtkLabel" id="label-app-logger">
|
||||
<property name="label" translatable="1">App</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkNotebookPage">
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="child">
|
||||
<object class="GtkTreeView" id="treeview-gst-logger"/>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="tab">
|
||||
<object class="GtkLabel" id="label-gst-logger">
|
||||
<property name="label" translatable="1">GST</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkNotebookPage">
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="child">
|
||||
<object class="GtkTreeView" id="treeview-msg-logger"/>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="tab">
|
||||
<object class="GtkLabel" id="label-messages-logger">
|
||||
<property name="label" translatable="1">Messages</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::app::GPSApp;
|
||||
use crate::logger;
|
||||
use crate::ui::treeview;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{gio, glib};
|
||||
|
@ -18,18 +19,37 @@ fn reset_logger_list(logger_list: &TreeView) {
|
|||
String::static_type(),
|
||||
String::static_type(),
|
||||
String::static_type(),
|
||||
String::static_type(),
|
||||
String::static_type(),
|
||||
]);
|
||||
logger_list.set_model(Some(&model));
|
||||
}
|
||||
|
||||
pub fn setup_logger_list(app: &GPSApp) {
|
||||
treeview::add_column_to_treeview(app, "treeview-logger", "TIME", 0, false);
|
||||
treeview::add_column_to_treeview(app, "treeview-logger", "LEVEL", 1, false);
|
||||
treeview::add_column_to_treeview(app, "treeview-logger", "LOG", 2, true);
|
||||
pub fn setup_logger_list(app: &GPSApp, logger_name: &str, log_type: logger::LogType) {
|
||||
match log_type {
|
||||
logger::LogType::App => {
|
||||
treeview::add_column_to_treeview(app, logger_name, "TIME", 0, false);
|
||||
treeview::add_column_to_treeview(app, logger_name, "LEVEL", 1, false);
|
||||
treeview::add_column_to_treeview(app, logger_name, "LOG", 2, true);
|
||||
}
|
||||
logger::LogType::Gst => {
|
||||
treeview::add_column_to_treeview(app, logger_name, "TIME", 0, false);
|
||||
treeview::add_column_to_treeview(app, logger_name, "LEVEL", 1, false);
|
||||
treeview::add_column_to_treeview(app, logger_name, "CATEGORY", 2, false);
|
||||
treeview::add_column_to_treeview(app, logger_name, "FILE", 3, false);
|
||||
treeview::add_column_to_treeview(app, logger_name, "LOG", 4, true);
|
||||
}
|
||||
logger::LogType::Message => {
|
||||
treeview::add_column_to_treeview(app, logger_name, "TIME", 0, false);
|
||||
treeview::add_column_to_treeview(app, logger_name, "LEVEL", 1, false);
|
||||
treeview::add_column_to_treeview(app, logger_name, "LOG", 2, true);
|
||||
}
|
||||
}
|
||||
|
||||
let logger_list: TreeView = app
|
||||
.builder
|
||||
.object("treeview-logger")
|
||||
.expect("Couldn't get treeview-logger");
|
||||
.object(logger_name)
|
||||
.expect("Couldn't get treeview-app-logger");
|
||||
reset_logger_list(&logger_list);
|
||||
|
||||
let gesture = gtk::GestureClick::new();
|
||||
|
@ -59,17 +79,45 @@ pub fn setup_logger_list(app: &GPSApp) {
|
|||
logger_list.add_controller(gesture);
|
||||
}
|
||||
|
||||
pub fn add_to_logger_list(app: &GPSApp, log_entry: &str) {
|
||||
fn log_tree_id_from_log_type(log_type: logger::LogType) -> String {
|
||||
match log_type {
|
||||
logger::LogType::App => String::from("treeview-app-logger"),
|
||||
logger::LogType::Gst => String::from("treeview-gst-logger"),
|
||||
logger::LogType::Message => String::from("treeview-msg-logger"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_to_logger_list(app: &GPSApp, log_type: logger::LogType, log_entry: &str) {
|
||||
let log_tree_name = log_tree_id_from_log_type(log_type.clone());
|
||||
let logger_list: TreeView = app
|
||||
.builder
|
||||
.object("treeview-logger")
|
||||
.expect("Couldn't get treeview-logger");
|
||||
.object(log_tree_name.as_str())
|
||||
.expect("Couldn't get treeview");
|
||||
if let Some(model) = logger_list.model() {
|
||||
let list_store = model
|
||||
.dynamic_cast::<ListStore>()
|
||||
.expect("Could not cast to ListStore");
|
||||
let log: Vec<&str> = log_entry.splitn(3, ' ').collect();
|
||||
list_store.insert_with_values(Some(0), &[(0, &log[0]), (1, &log[1]), (2, &log[2])]);
|
||||
if log_type == logger::LogType::Gst {
|
||||
let log: Vec<&str> = log_entry.splitn(5, '\t').collect();
|
||||
list_store.insert_with_values(
|
||||
Some(0),
|
||||
&[
|
||||
(0, &log[0]),
|
||||
(1, &log[1]),
|
||||
(2, &log[2]),
|
||||
(3, &log[3]),
|
||||
(4, &log[4]),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
let log: Vec<&str> = log_entry.splitn(3, ' ').collect();
|
||||
let mut indexed_vec: Vec<(u32, &dyn ToValue)> = Vec::new();
|
||||
|
||||
for (index, item) in log.iter().enumerate() {
|
||||
indexed_vec.push((index as u32, item));
|
||||
}
|
||||
list_store.insert_with_values(Some(0), &indexed_vec);
|
||||
}
|
||||
// Scroll to the first element.
|
||||
if let Some(model) = logger_list.model() {
|
||||
if let Some(iter) = model.iter_first() {
|
||||
|
|
|
@ -82,5 +82,16 @@ pub fn display_settings(app: &GPSApp) {
|
|||
dialog.close();
|
||||
});
|
||||
|
||||
let widget = gtk::Entry::new();
|
||||
widget.set_text(settings::Settings::gst_log_level().as_str());
|
||||
widget.connect_changed(glib::clone!(@weak widget => move |c| {
|
||||
let mut settings = settings::Settings::load_settings();
|
||||
settings.preferences.insert("gst_log_level".to_string(), c.text().to_string());
|
||||
settings::Settings::save_settings(&settings);
|
||||
}));
|
||||
let widget = widget
|
||||
.dynamic_cast::<gtk::Widget>()
|
||||
.expect("Should be a widget");
|
||||
add_settings_widget(&grid, "GST Log level", &widget, 2);
|
||||
dialog.show();
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ pub fn property_to_widget<F: Fn(String, String) + 'static>(
|
|||
glib::ParamSpecInt64::static_type(),
|
||||
glib::ParamSpecUInt64::static_type(),
|
||||
glib::ParamSpecString::static_type(),
|
||||
glib::ParamSpecFloat::static_type(),
|
||||
]
|
||||
.contains(&t) =>
|
||||
{
|
||||
|
@ -113,7 +114,7 @@ pub fn property_to_widget<F: Fn(String, String) + 'static>(
|
|||
let param = param
|
||||
.clone()
|
||||
.downcast::<glib::ParamSpecFlags>()
|
||||
.expect("Should be a ParamSpecEnum");
|
||||
.expect("Should be a ParamSpecFlags");
|
||||
let flags = param.flags_class();
|
||||
for value in flags.values() {
|
||||
combo.append_text(&format!(
|
||||
|
|
Loading…
Reference in a new issue