Compare commits

...

42 commits
0.3.3 ... main

Author SHA1 Message Date
Stéphane Cerveau c309e94db2 graphbook: Use a scrolledwindow instead of ViewPort
In order to support the zoom, the graphbook must be
a scrolled window and not a ViewPort
2024-05-27 13:43:19 +02:00
Stéphane Cerveau c60fbf8342 player: use default value 2024-05-27 13:41:47 +02:00
Stéphane Cerveau 671fd8ffd2 graphview: allow to zoom in or zoom out in the graph 2024-05-27 12:26:07 +02:00
Stéphane Cerveau b8a92586d4 gtk: update to 0.8.2
update other packages to latter versions with cargo update
2024-05-21 11:40:07 +02:00
Stéphane Cerveau 1f200f4f30 app: update to gtk 0.8.0 and gst 0.22.2
In the change, the glib::channel has been dropped to use
async_io which achieves the same to receive message
for the logger and display it in the treeview.
Another message is received when a new gtkpaintablesink
has been instanciated.
2024-03-14 16:34:55 +01:00
Stéphane Cerveau 44d64ccdc4 cargo: update gtk and gst crate
gtk4 0.7.3
gst 0.21.3
gst-plugins-gtk 0.11.3
2024-01-25 20:04:38 +01:00
Stéphane Cerveau 3985458832 properties: can now support ParamSpecFloat 2024-01-23 10:29:57 +01:00
Stéphane Cerveau 7a70feedba pages: update the pages with release 0.3.5 2024-01-05 16:34:13 +01:00
Stéphane Cerveau 88afa4a99e release: 0.3.5 2024-01-05 15:36:00 +01:00
Stéphane Cerveau d32c75b639 ci: change to macos 13 runner 2024-01-05 14:37:13 +01:00
Stéphane Cerveau 7789588aef ci: fix yaml issue detected by pre-commit 2024-01-05 11:25:03 +01:00
Stéphane Cerveau b1ad0e958b graphmanager: fix typos detected by pre-commit 2024-01-05 11:25:03 +01:00
Stéphane Cerveau 9c8a578e05 gps: fix typos and format detected by pre-commit 2024-01-05 11:25:03 +01:00
Stéphane Cerveau 3e15b7cecb graphmanager: remove link export 2024-01-05 11:25:03 +01:00
Stéphane Cerveau 11cf962bfd ci: add pre-commit job 2024-01-05 11:25:03 +01:00
Stéphane Cerveau a25f0499c8 macos: disabled ges build
Due to an error with python, disable GES build
for now.
2024-01-05 09:32:40 +01:00
Stéphane Cerveau 51769d6061 logger: support variable length for logger split 2024-01-04 15:20:14 +00:00
Stéphane Cerveau af317eee96 app: rename gst_pipeline_studio to gst-pipeline-studio 2024-01-04 14:32:17 +01:00
Stéphane Cerveau 3aded523c2 element: add property special case for float 2024-01-03 13:52:04 +01:00
Stéphane Cerveau d3005335b8 logger: add function name in the logs 2024-01-03 13:25:49 +01:00
Stéphane Cerveau 18458e3465 properties: fix expect for ParamSpecFlags 2024-01-03 13:25:49 +01:00
Stéphane Cerveau a6f03db8f6 element: element property special convert for enum and flags
In element_property, add special case enum and flags
property to use a better value such as nick or the
proper u32 flags
2024-01-03 13:25:49 +01:00
Stéphane Cerveau bed8d6a58e logger: fix clippy complain 2024-01-02 14:51:38 +01:00
Stéphane Cerveau c5f9cac444 app: support cmd line to open a custom pipeline
Add a way to open a pipeline from the cmd line
2024-01-02 14:51:03 +01:00
Stéphane Cerveau 30baa56881 wix: installer to a different UI profile
In order to remove minimal and full option
uses only a minimal profile.
2023-12-22 15:08:18 +01:00
Stéphane Cerveau 13165fa9c0 ci: use release build for windows
Create an image with release build of
gstreamer and gtk
2023-12-22 11:40:59 +01:00
Stéphane Cerveau 6ca3059914 ci: set the bundler version to 2.4.22 2023-12-19 11:20:56 +01:00
Stéphane Cerveau f4019fd2af wix: change wix banner and dialog bmp 2023-12-19 10:29:32 +01:00
Stéphane Cerveau 8c6cda2e92 player: feed the app with gst logs
Get the debug callback to retrieve the gst logs
and display it in the debug tab.
2023-11-30 11:54:15 +01:00
Stéphane Cerveau 9c03de5d00 player: keep a reference to bus_watch_guard
In order to receive the message from the bus
the API enforces to keep a reference to the bus_watch_guard
otherwise the watch gets lost.
2023-11-30 11:52:28 +01:00
Stéphane Cerveau 24121856ee app: use the channel to receive other logs
Add another logger to receive message, events or GST logs
2023-11-30 11:52:28 +01:00
Stéphane Cerveau e410289a13 ui: support multiple tab in debug section
Add GST logs, message, events in the debug section
2023-11-29 23:16:06 +01:00
Stéphane Cerveau 2601454143 Update README.md to include the website 2023-10-05 12:28:41 +00:00
Stéphane Cerveau f47b0624fd appdata: add a release description 2023-09-29 15:48:02 +02:00
Stéphane Cerveau b2c6a8bc2a pages: update the pages with release 0.3.4 2023-09-28 19:06:06 +02:00
Stéphane Cerveau d002e2811f release: 0.3.4 2023-09-28 16:45:01 +02:00
Stéphane Cerveau 0148a43946 windows: install the share folder from gtk/gst 2023-09-28 16:45:01 +02:00
Stéphane Cerveau e12fecf971 settings: create the default app folder
To avoid a crash if the settings folder is
not present and the log can not be created properly
2023-09-28 16:45:01 +02:00
Stéphane Cerveau 886c099dba data: update homepage in appdata
This appdata will be the mirror of what we
can see on
https://flathub.org/apps/org.freedesktop.dabrain34.GstPipelineStudio
2023-09-28 16:44:42 +02:00
Stéphane Cerveau f81bedb71a data: update the screenshot for 0.3.3 2023-09-24 12:57:09 +02:00
Stéphane Cerveau 2787211f0d release: update release version fetcher 2023-09-23 09:05:30 +02:00
Stéphane Cerveau 98d6451e74 pages: update the pages with release 0.3.3 2023-09-22 15:32:09 +02:00
50 changed files with 1608 additions and 743 deletions

View file

@ -22,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
@ -60,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
@ -90,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
@ -120,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,11 +239,8 @@ flatpak:
macos installer stable:
stage: test
needs:
- job: "windows rust docker stable"
artifacts: false
tags:
- gst-macos-11.1
- gst-macos-13
before_script:
- pip3 install --upgrade pip
# Make sure meson is up to date
@ -274,7 +283,7 @@ pages:
image: ruby:2.7
stage: deploy
script:
- gem install bundler
- gem install bundler -v 2.4.22
- bundle install
- bundle exec jekyll build -d public
artifacts:

29
.pre-commit-config.yaml Normal file
View 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

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,15 @@
[package]
name = "gst_pipeline_studio"
version = "0.3.3"
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"

View file

@ -108,6 +108,20 @@
## 0.3.3
### app
- [x] Fix MacOs GTK runtime depedencies
- [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

View file

@ -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

View file

@ -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.

View file

@ -1 +1 @@
0.3.3
0.3.5

View file

@ -15,18 +15,31 @@ 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["PKG_CONFIG_PATH"] = (
os.path.join(MESON_BUILD_ROOT, "meson-uninstalled")
+ os.pathsep
+ env.get("PKG_CONFIG_PATH", "")
)
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)

View file

@ -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")])

View file

@ -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": [

View file

@ -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"

View file

@ -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"

View file

@ -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 " +

View file

@ -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

View file

@ -3,7 +3,7 @@
GstPipelineStudio Website
====================
shamelessy stolen CSS from Pipewire
shamelessly stolen CSS from Pipewire
*/

View file

@ -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>

View file

@ -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

View file

@ -13,7 +13,7 @@
<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.3 is out, checkout the <a
<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>
@ -22,7 +22,13 @@
<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/9ed4a61b3f4de01555599b6f1f60b9f9/GstPipelineStudio-0.3.2.msi">Windows</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>

View file

@ -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
@ -32,6 +32,7 @@ GSTREAMER_OPTS="
-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
@ -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,7 +94,7 @@ 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"

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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>

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
installer/wix/wixdialog.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

View file

@ -1,5 +1,5 @@
project('gst_pipeline_studio',
version: '0.3.3',
project('gst-pipeline-studio',
version: '0.3.5',
meson_version: '>= 0.63.0',
default_options: [ 'warning_level=2',
],

View file

@ -5,6 +5,8 @@
- cargo.toml
- 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
- Fetch the package from the `linux release` job or you can make it manually with:

View file

@ -150,7 +150,7 @@ impl GPSApp {
.insert(paned_name.to_string(), paned.position());
}
pub fn on_startup(application: &gtk::Application) {
pub fn on_startup(application: &gtk::Application, pipeline_desc: &String) {
// Create application and error out if that fails for whatever reason
let app = match GPSApp::new(application) {
Ok(app) => app,
@ -160,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
@ -449,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| {
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);
GPSUI::logger::add_to_logger_list(&app, msg.0, &msg.1);
}
glib::ControlFlow::Continue
});
@ -511,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);
},
);
@ -604,10 +605,10 @@ impl GPSApp {
GPSUI::elements::setup_favorite_list(self);
// Setup the favorite list
GPSUI::elements::setup_elements_list(self);
if pipeline_desc.is_empty() {
let _ = self
.load_graph(
Settings::default_graph_file_path()
Settings::graph_file_path()
.to_str()
.expect("Unable to convert to string"),
true,
@ -615,6 +616,11 @@ impl GPSApp {
.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
@ -775,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(())
}
}

View file

@ -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");

View file

@ -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)
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_or_default();
.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());

View file

@ -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,9 +142,10 @@ 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| {
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();
@ -130,6 +156,7 @@ impl Player {
.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();

View file

@ -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"),
)

View file

@ -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();
drag_controller.connect_drag_begin(|drag_controller, x, y| {
let widget = drag_controller
.widget()
.dynamic_cast::<Self::Type>()
.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");
*drag_state = if target.ancestor(Port::static_type()).is_some() {
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.
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
}
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| {
});
drag_controller.connect_drag_update(|drag_controller, x, y| {
let widget = drag_controller
.widget()
.dynamic_cast::<Self::Type>()
.dynamic_cast::<super::GraphView>()
.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();
}
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: &gtk::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: &gtk::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(&current_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: &gtk::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<&gtk::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: &gtk::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 {

View file

@ -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;

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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',

View file

@ -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) {
if let Err(e) = create_dir_all(s) {
GPS_ERROR!(
"Error while trying to build settings snapshot_directory '{}': {}",
parent_dir.display(),
s.display(),
e
);
}
}
}
}
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 = glib::user_config_dir();
path.push(config::APP_ID);
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);

View file

@ -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>
</child>
<child>
<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-logger"/>
<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>

View file

@ -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");
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();
list_store.insert_with_values(Some(0), &[(0, &log[0]), (1, &log[1]), (2, &log[2])]);
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() {

View file

@ -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();
}

View file

@ -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!(