Commit e1da2c8b authored by Claes Sjofors's avatar Claes Sjofors

Support for streaming video added, and xtt command 'open graph' with list instances

parent 99c21730
......@@ -122,7 +122,7 @@ log_done =
#cinc := -I$(inc_dir) -I$(einc_dir) -I$(hw_source) -I$(os_source) -I$(co_source) -I/usr/X11R6/include -I$(jdk)/include -I$(jdk)/include/linux \
`pkg-config --cflags gtk+-2.0` -DPREFIX=\"/usr/local\" -DSYSCONFDIR=\"/etc\" -DDATADIR=\"/usr/share\" -DLIBDIR=\"/usr/lib\" $(cmysql) $(cgtk) $(clibusb)
csetos := $(pwre_conf_cc_define)
cinc := -I$(inc_dir) -I$(einc_dir) -I$(hw_source) -I$(os_source) -I$(co_source) $(pwre_conf_incdir) $(pwre_conf_incdirgtk)
cinc := -I$(inc_dir) -I$(einc_dir) -I$(hw_source) -I$(os_source) -I$(co_source) $(pwre_conf_incdir) $(pwre_conf_incdirgtk) $(pwre_conf_incdirgst)
rm := rm
cp := cp
cpflags :=
......
......@@ -182,6 +182,9 @@ pwre_config_check_lib()
elif test $3 == "gtk"; then
conf_libgtk=$conf_libgtk" \\\`pkg-config --libs gtk+-2.0\\\`"
conf_incdirgtk=$conf_incdirgtk" \\\`pkg-config --cflags gtk+-2.0\\\`"
elif test $3 == "gst"; then
conf_libgst=$conf_libgst" \\\`pkg-config --libs gstreamer-interfaces-0.10 gstreamer-0.10\\\`"
conf_incdirgst=$conf_incdirgst" \\\`pkg-config --cflags gstreamer-interfaces-0.10 gstreamer-0.10\\\`"
elif test $3 == "motif"; then
conf_libmotif=$conf_libmotif" -lImlib -lMrm -lXm -lXpm -lXt -lX11 -lXext -lXp -lSM -lICE"
else
......@@ -271,8 +274,10 @@ conf_libwmq=""
conf_libpnak=""
conf_libgtk=""
conf_libmotif=""
conf_libgst=""
conf_libdir=""
conf_incdirgtk=""
conf_incdirgst=""
let inc_cnt=0
let lib_cnt=0
let i=0
......@@ -411,6 +416,7 @@ if [ $pwre_hw == "hw_arm" ] && [ $ebuild -eq 1 ]; then
echo "export pwre_conf_libpnak=\"$conf_libpnak\"" >> $cfile
echo "export pwre_conf_libgtk=\"$conf_libgtk\"" >> $cfile
echo "export pwre_conf_libmotif=\"$conf_libmotif\"" >> $cfile
echo "export pwre_conf_libgst=\"$conf_libgtk\"" >> $cfile
echo "export pwre_conf_libdir=\"$conf_libdir\"" >> $cfile
echo "export pwre_conf_incdir=\"$conf_incdir\"" >> $cfile
echo "export pwre_conf_incdirgtk=\"$conf_incdirgtk\"" >> $cfile
......@@ -460,6 +466,8 @@ else
pwre_config_check_lib powerlinkcn POWERLINKCN lib powerlinkcn 1 "$epl/buildcn/Examples/X86/Generic/powerlink_user_lib/libpowerlink.a"
pwre_config_check_lib libpcap LIBPCAP lib libpcap 1 "/usr/lib/libpcap.so:/usr/lib/$hwpl-linux-$gnu/libpcap.so"
pwre_config_check_lib librsvg LIBRSVG lib librsvg 1 "/usr/lib/librsvg-2.so:/usr/lib/$hwpl-linux-$gnu/librsvg-2.so"
pwre_config_check_include gst GST 1 "/opt/gstreamer-sdk/include/gstreamer-0.10/gst/gst.h"
pwre_config_check_lib gst GST gst gst 0 "/opt/gstreamer-sdk/lib/libgstreamer-0.10.so"
if [ $pwre_hw == "hw_arm" ]; then
pwre_config_check_lib libpiface LIBPIFACE lib libpiface 1 "/usr/local/lib/libpiface-1.0.a"
pwre_config_check_include piface PIFACE 1 "/usr/local/include/libpiface-1.0/pfio.h"
......@@ -515,9 +523,11 @@ else
echo "export pwre_conf_libwmq=\"$conf_libwmq\"" >> $cfile
echo "export pwre_conf_libpnak=\"$conf_libpnak\"" >> $cfile
echo "export pwre_conf_libgtk=\"$conf_libgtk\"" >> $cfile
echo "export pwre_conf_libgst=\"$conf_libgst\"" >> $cfile
echo "export pwre_conf_libmotif=\"$conf_libmotif\"" >> $cfile
echo "export pwre_conf_libdir=\"$conf_libdir\"" >> $cfile
echo "export pwre_conf_incdir=\"$conf_incdir\"" >> $cfile
echo "export pwre_conf_incdirgtk=\"$conf_incdirgtk\"" >> $cfile
echo "export pwre_conf_incdirgst=\"$conf_incdirgst\"" >> $cfile
fi
......@@ -130,6 +130,8 @@ SObject pwrb:Class
Object Object $Attribute 8
Body SysBody
Attr TypeRef = "pwrs:Type-$AttrRef"
Attr Flags |= PWR_MASK_ARRAY
Attr Elements = 4
EndBody
EndObject
!/**
......@@ -137,13 +139,26 @@ SObject pwrb:Class
! - Menu, a menu is created. For window managers with one
! common menu row, only one menu is displayed.
! - Scrollbars, scrollbars are viewed.
! - Excangable, a Ge graph can be exchanged with the 'set subwindow' command (NYI).
! - Exchangable, a Ge graph can be exchanged with the 'set subwindow' command (NYI).
!*/
Object Options $Attribute 9
Body SysBody
Attr TypeRef = "pwrb:Type-MultiViewElemOptionsMask"
EndBody
EndObject
!/**
! Determines the borders of a Ge graph. The first two elements are the
! coordinates for the upper left corner, and the two last, the coordinates for
! the lower right corner. These values will override the x0, y0, x1 and y1
! values in Graph Attributes for the graph.
!*/
Object Borders $Attribute 10
Body SysBody
Attr TypeRef = "pwrs:Type-$Float32"
Attr Flags |= PWR_MASK_ARRAY
Attr Elements = 4
EndBody
EndObject
EndObject
EndObject
EndSObject
......@@ -159,11 +159,13 @@ SObject pwrb:Class
EndObject
!/**
! Name of an object, if action is opening a class graph for a specific object.
! In Ge graphs, first element replaces "$object", second "$object2" etc.
!*/
Object Object $Attribute 13
Body SysBody
Attr TypeRef = "pwrs:Type-$Objid"
Attr Flags = 0
Attr Flags |= PWR_MASK_ARRAY
Attr Elements = 4
EndBody
EndObject
!/**
......@@ -184,6 +186,19 @@ SObject pwrb:Class
EndBody
EndObject
!/**
! Determines the border of a Ge graph. The first two elements are the
! coordinates for the upper left corner, and the two last, the coordinates for
! the lower right corner. These values will override the x0, y0, x1 and y1
! values in Graph Attributes for the graph.
!*/
Object Borders $Attribute 16
Body SysBody
Attr TypeRef = "pwrs:Type-$Float32"
Attr Flags |= PWR_MASK_ARRAY
Attr Elements = 4
EndBody
EndObject
!/**
! @Summary Configuration status.
! Configuration status.
! Status in the development environment for an hierarchy,
......
!
! Proview Open Source Process Control.
! Copyright (C) 2005-2014 SSAB AB.
!
! This file is part of Proview.
!
! This program is free software; you can redistribute it and/or
! modify it under the terms of the GNU General Public License as
! published by the Free Software Foundation, either version 2 of
! the License, or (at your option) any later version.
!
! This program is distributed in the hope that it will be useful
! but WITHOUT ANY WARRANTY; without even the implied warranty of
! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
! GNU General Public License for more details.
!
! You should have received a copy of the GNU General Public License
! along with Proview. If not, see <http://www.gnu.org/licenses/>
!
! Linking Proview statically or dynamically with other modules is
! making a combined work based on Proview. Thus, the terms and
! conditions of the GNU General Public License cover the whole
! combination.
!
! In addition, as a special exception, the copyright holders of
! Proview give you permission to, from the build function in the
! Proview Configurator, combine Proview with modules generated by the
! Proview PLC Editor to a PLC program, regardless of the license
! terms of these modules. You may copy and distribute the resulting
! combined work under the terms of your choice, provided that every
! copy of the combined work is accompanied by a complete copy of
! the source code of Proview (the version used to produce the
! combined work), being distributed under the terms of the GNU
! General Public License plus this exception.
!
! pwrb_c_xttvideo.wb_load -- Defines the class XttVideo.
!
SObject pwrb:Class
!/**
! @Version 1.0
! @Group Operator,NodeConfiguration
! @Summary Configure a video stream
! The XttVideo object configures a video stream.
!
! @b See also
! @classlink OpPlace pwrb_opplace.html
!*/
!
Object XttVideo $ClassDef 653
Body SysBody
Attr Editor = pwr_eEditor_AttrEd
Attr Method = pwr_eMethod_Standard
EndBody
Object RtBody $ObjBodyDef 1
Body SysBody
Attr StructName = "XttVideo"
EndBody
!/**
! Description of the object.
!*/
Object Description $Attribute 1
Body SysBody
Attr TypeRef = "pwrs:Type-$String80"
EndBody
EndObject
!/**
! URL to the camera image.
!*/
Object URL $Attribute 2
Body SysBody
Attr TypeRef = "pwrs:Type-$URL"
EndBody
EndObject
!/**
! Title of the window.
!*/
Object Title $Attribute 3
Body SysBody
Attr TypeRef = "pwrs:Type-$String40"
EndBody
EndObject
!/**
! Text of the pushbutton in the operator window.
!*/
Object ButtonText $Attribute 4
Body SysBody
Attr TypeRef = "pwrs:Type-$String40"
EndBody
EndObject
!/**
! X position in pixel for the window.
!*/
Object X $Attribute 6
Body SysBody
Attr TypeRef = "pwrs:Type-$Int32"
Attr Flags = 0
EndBody
EndObject
!/**
! Y position in pixel for the window.
!*/
Object Y $Attribute 7
Body SysBody
Attr TypeRef = "pwrs:Type-$Int32"
Attr Flags = 0
EndBody
EndObject
!/**
! Width of the window in pixel.
!*/
Object Width $Attribute 8
Body SysBody
Attr TypeRef = "pwrs:Type-$Int32"
Attr Flags = 0
EndBody
EndObject
!/**
! Height of the window in pixel.
!*/
Object Height $Attribute 9
Body SysBody
Attr TypeRef = "pwrs:Type-$Int32"
Attr Flags = 0
EndBody
EndObject
!/**
! Options for the video.
!
! - FullScreen Open the graph as full screen without frame.
! - Maximize Open the graph maximized, not covering the operator window (NYI).
! - FullMaximize Open the graph maximized, covering the operator window.
! - Iconify Open the graph iconified.
! - ControlPanel Show a control panel with progress bar, Play, Pause and Stop buttons.
!*/
Object Options $Attribute 15
Body SysBody
Attr TypeRef = "pwrb:Type-VideoOptionsMask"
Attr Flags = 0
EndBody
EndObject
EndObject
Object Template XttVideo
EndObject
EndObject
EndSObject
......@@ -129,6 +129,16 @@ SObject pwrb:Type
Attr Value = 7
EndBody
EndObject
!/**
! Video.
!*/
Object Video $Value
Body SysBody
Attr PgmName = "Video"
Attr Text = "Video"
Attr Value = 8
EndBody
EndObject
EndObject
EndSObject
......
!
! Proview Open Source Process Control.
! Copyright (C) 2005-2014 SSAB AB.
!
! This file is part of Proview.
!
! This program is free software; you can redistribute it and/or
! modify it under the terms of the GNU General Public License as
! published by the Free Software Foundation, either version 2 of
! the License, or (at your option) any later version.
!
! This program is distributed in the hope that it will be useful
! but WITHOUT ANY WARRANTY; without even the implied warranty of
! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
! GNU General Public License for more details.
!
! You should have received a copy of the GNU General Public License
! along with Proview. If not, see <http://www.gnu.org/licenses/>
!
! Linking Proview statically or dynamically with other modules is
! making a combined work based on Proview. Thus, the terms and
! conditions of the GNU General Public License cover the whole
! combination.
!
! In addition, as a special exception, the copyright holders of
! Proview give you permission to, from the build function in the
! Proview Configurator, combine Proview with modules generated by the
! Proview PLC Editor to a PLC program, regardless of the license
! terms of these modules. You may copy and distribute the resulting
! combined work under the terms of your choice, provided that every
! copy of the combined work is accompanied by a complete copy of
! the source code of Proview (the version used to produce the
! combined work), being distributed under the terms of the GNU
! General Public License plus this exception.
!
! pwrb_videloptionsmask.wb_load -- Defines the mask type VideoOptionsMask
!
SObject pwrb:Type
!/**
! @Version 1.0
! @Group Types
! Bitmask for xtt video options.
!
! @b See also
! @classlink XttVideo pwrb_xttvideo.html
!*/
Object VideoOptionsMask $TypeDef 79
Body SysBody
Attr Type = pwr_eType_Mask
Attr Size = 4
Attr TypeRef = "pwrs:Type-$Mask"
Attr Elements = 1
EndBody
!/**
! Full screen.
!*/
Object FullSceen $Bit
Body SysBody
Attr PgmName = "FullScreen"
Attr Text = "FullScreen"
Attr Value = 1
EndBody
EndObject
!/**
! Maximize.
!*/
Object Maximize $Bit
Body SysBody
Attr PgmName = "Maximize"
Attr Text = "Maximize"
Attr Value = 2
EndBody
EndObject
!/**
! Full maximize.
!*/
Object FullMaximize $Bit
Body SysBody
Attr PgmName = "FullMaximize"
Attr Text = "FullMaximize"
Attr Value = 4
EndBody
EndObject
!/**
! Iconify.
!*/
Object Iconify $Bit
Body SysBody
Attr PgmName = "Iconify"
Attr Text = "Iconify"
Attr Value = 8
EndBody
EndObject
!/**
! Display a control panel with Play, Pause and Stop buttons.
!*/
Object ControlPanel $Bit
Body SysBody
Attr PgmName = "ControlPanel"
Attr Text = "ControlPanel"
Attr Value = 16
EndBody
EndObject
!/**
! Add progress bar to control panel.
!*/
Object ProgressBar $Bit
Body SysBody
Attr PgmName = "ProgressBar"
Attr Text = "ProgressBar"
Attr Value = 32
EndBody
EndObject
EndObject
EndSObject
......@@ -481,6 +481,7 @@ palette NavigatorPalette
class OpPlace
class XttGraph
class XttMultiView
class XttVideo
class WebBrowserConfig
class WebGraph
class WebLink
......
......@@ -7,7 +7,7 @@ ifeq ($(export_type),exp)
$(pwr_eobj)/rt_io_user.o \
$(pwre_conf_libdir) $(pwre_conf_libpwrxttgtk) $(pwre_conf_libpwrxtt) \
$(pwre_conf_libpwrxttgtk) $(pwre_conf_libpwrxtt) \
$(pwre_conf_libgtk) \
$(pwre_conf_libgst) $(pwre_conf_libgtk) \
$(pwre_conf_libpwrrt) $(pwre_conf_lib)
else
......@@ -16,7 +16,7 @@ else
$(pwr_eobj)/rt_io_user.o \
$(pwre_conf_libdir) $(pwre_conf_libpwrxttgtk) $(pwre_conf_libpwrxtt) \
$(pwre_conf_libpwrxttgtk) $(pwre_conf_libpwrxtt) \
$(pwre_conf_libgtk) \
$(pwre_conf_libgst) $(pwre_conf_libgtk) \
$(pwre_conf_libpwrrt) $(pwre_conf_lib)
endif
......
......@@ -366,6 +366,11 @@ void Xtt::activate_opengraph()
sprintf( cmd, "open mult /name=%s", vname);
xnav->command( cmd);
return;
case pwr_cClass_XttVideo:
// Open video
sprintf( cmd, "open vide /obj=%s", vname);
xnav->command( cmd);
return;
case pwr_cClass_DsTrend:
case pwr_cClass_DsTrendCurve:
// Open trend
......
......@@ -194,10 +194,16 @@ Graph::Graph(
cdh_StrncpyCutOff( name, xn_name, sizeof(name), 1);
strcpy( default_path, xn_default_path);
memset( arglist_stack, 0, sizeof(arglist_stack));
if ( xn_object_name)
strcpy( object_name, xn_object_name);
else
strcpy( object_name, "");
for ( unsigned int i = 0; i < sizeof(object_name)/sizeof(object_name[0]); i++)
strcpy( object_name[i], "");
if ( xn_object_name) {
if ( strchr( xn_object_name, ',') != 0)
dcli_parse( xn_object_name, ",", "",
(char *) object_name, sizeof( object_name)/sizeof(object_name[0]),
sizeof( object_name[0]), 0);
else
strcpy( object_name[0], xn_object_name);
}
strcpy( filename, "");
strcpy( systemname, "");
......@@ -3478,7 +3484,7 @@ int Graph::init_trace()
grow->grow_trace_setup();
// Look for object graph
if ( strcmp( object_name, "") != 0)
if ( strcmp( object_name[0], "") != 0)
init_object_graph(0);
sts = grow_TraceInit( grow->ctx, graph_trace_connect_bc,
......@@ -3486,7 +3492,7 @@ int Graph::init_trace()
graph_trace_ctrl_bc);
// Look for object graph
if ( strcmp( object_name, "") != 0)
if ( strcmp( object_name[0], "") != 0)
init_object_graph(1);
trace_started = 1;
......@@ -4290,26 +4296,30 @@ void Graph::get_command( char *in, char *out, GeDyn *dyn)
char *s0 = in;
char str[500];
pwr_tOName oname;
if ( grow->stack_cnt == 0)
strcpy( oname, object_name);
pwr_tAName oname[4];
if ( grow->stack_cnt == 0) {
for ( int i = 0; i < 4; i++)
strcpy( oname[i], object_name[i]);
}
else {
grow_GetOwner( grow->ctx, oname);
grow_GetOwner( grow->ctx, oname[0]);
if ( strcmp( object_name, "") != 0) {
if ( strcmp( object_name[0], "") != 0) {
pwr_tOName n;
t0 = n;
s0 = oname;
s0 = oname[0];
while ( (s = strstr( s0, "$object"))) {
strncpy( t0, s0, s-s0);
t0 += s - s0;
strcpy( t0, object_name);
t0 += strlen(object_name);
strcpy( t0, object_name[0]);
t0 += strlen(object_name[0]);
s0 = s + strlen("$object");
}
cdh_Strcpy( t0, s0);
strcpy( oname, n);
strcpy( oname[0], n);
}
for ( int i = 1; i < 4; i++)
strcpy( oname[i], object_name[i]);
}
s0 = in;
if ( dyn && (dyn->total_dyn_type1 & ge_mDynType1_HostObject ||
......@@ -4329,23 +4339,39 @@ void Graph::get_command( char *in, char *out, GeDyn *dyn)
}
cdh_Strcpy( t0, s0);
if ( strcmp( oname, "") == 0) {
if ( strcmp( oname[0], "") == 0) {
strcpy( out, str);
}
s0 = str;
}
else if ( strcmp( oname, "") == 0) {
else if ( strcmp( oname[0], "") == 0) {
cdh_Strcpy( out, in);
}
if ( strcmp( oname, "") != 0) {
if ( strcmp( oname[0], "") != 0) {
t0 = out;
while ( (s = strstr( s0, "$object"))) {
int idx;
char *sidx = s + strlen("$object");
switch ( *sidx) {
case '2':
idx = 1;
break;
case '3':
idx = 2;
break;
case '4':
idx = 3;
break;
default:
idx = 0;
}
cdh_Strncpy( t0, s0, s-s0);
t0 += s - s0;
strcpy( t0, oname);
t0 += strlen(oname);
s0 = s + strlen("$object");
strcpy( t0, oname[idx]);
t0 += strlen(oname[idx]);
s0 = s + strlen("$object") + (idx > 0 ? 1 : 0);
}
cdh_Strcpy( t0, s0);
......@@ -4535,23 +4561,40 @@ graph_eDatabase Graph::parse_attr_name( char *name, char *parsed_name,
}
if ( (s = strstr( str, "$object"))) {
char oname[256];
pwr_tAName oname[4];
for ( int i = 1; i < 4; i++)
strcpy( oname[i], object_name[i]);
if ( grow->stack_cnt == 0)
strcpy( oname, object_name);
strcpy( oname[0], object_name[0]);
else {
grow_GetOwner( grow->ctx, oname);
grow_GetOwner( grow->ctx, oname[0]);
// Replace $object in oname (one level only)
char *s1;
if ( (s1 = strstr( oname, "$object"))) {
if ( (s1 = strstr( oname[0], "$object"))) {
strcpy( str1, s1 + strlen("$object"));
strcpy( s1, object_name);
strcat( oname, str1);
strcpy( s1, object_name[0]);
strcat( oname[0], str1);
}
}
strcpy( str1, s + strlen("$object"));
strcpy( s, oname);
int idx;
char *sidx = s + strlen("$object");
switch ( *sidx) {
case '2':
idx = 1;
break;
case '3':
idx = 2;
break;
case '4':
idx = 3;
break;
default:
idx = 0;
}
strcpy( str1, s + strlen("$object") + (idx > 0 ? 1 : 0));
strcpy( s, oname[idx]);
strcat( str, str1);
}
......@@ -4560,8 +4603,8 @@ graph_eDatabase Graph::parse_attr_name( char *name, char *parsed_name,
pwr_tOid oid;
pwr_tStatus sts;
if ( strcmp( object_name, "") != 0) {
sts = gdh_NameToObjid( object_name, &oid);
if ( strcmp( object_name[0], "") != 0) {
sts = gdh_NameToObjid( object_name[0], &oid);
if ( ODD(sts))
sts = gdh_GetNodeObject( oid.vid, &oid);
}
......
......@@ -444,7 +444,7 @@ class Graph {
GraphRecallBuff recall; //! Recall buffer for dynamics.
void *parent_ctx; //! Parent context.
char name[300]; //! Name.
pwr_tAName object_name; //! Name of object for class graphs.
pwr_tAName object_name[4]; //! Name of object for class graphs.
GraphGrow *grow; //! GraphGrow
GraphGrow *grow_stack[GRAPH_GROW_MAX]; //! Grow stack. Not used.
int grow_cnt; //! Number of grow in stack. Not used.
......
......@@ -183,7 +183,7 @@ int Graph::init_object_graph( int mode)
{
if ( strcmp( filename, "_none_.pwg") == 0)
{
if ( strcmp( object_name, "collect") == 0)
if ( strcmp( object_name[0], "collect") == 0)
{
sts = graph_object_collect_build( this, 0);
return sts;
......@@ -194,7 +194,7 @@ int Graph::init_object_graph( int mode)
if ( strcmp( filename, "_none_.pwg") == 0)
{
if ( strcmp( object_name, "collect") == 0)
if ( strcmp( object_name[0], "collect") == 0)
{
sts = graph_object_collect( this, 0);
return sts;
......@@ -224,7 +224,7 @@ int Graph::init_object_graph( int mode)
if ( is_type)
{
sts = gdh_NameToAttrref( pwr_cNObjid, object_name, &attrref);
sts = gdh_NameToAttrref( pwr_cNObjid, object_name[0], &attrref);
if ( EVEN(sts)) return sts;
if ( strcmp( classname, "float32") == 0) {
......@@ -252,7 +252,7 @@ int Graph::init_object_graph( int mode)
sts = gdh_ClassNameToId( classname, &classid);
if ( EVEN(sts)) return sts;
sts = gdh_NameToAttrref( pwr_cNObjid, object_name, &attrref);
sts = gdh_NameToAttrref( pwr_cNObjid, object_name[0], &attrref);
if ( EVEN(sts)) return sts;
for ( i = 0; graph_object_functions[i].classid; i++)
......
......@@ -58,6 +58,7 @@ typedef void *Widget;
#include "xtt_ge_gtk.h"
#include "xtt_trend_gtk.h"
#include "xtt_sevhist_gtk.h"
#include "xtt_stream_gtk.h"
#include "ge_graph_gtk.h"
#include "xtt_ev_gtk.h"
#include "xtt_evala_gtk.h"
......@@ -150,6 +151,11 @@ XttMultiViewGtk::~XttMultiViewGtk()
if ( sevhist[i])
delete sevhist[i];
}
for ( unsigned int i = 0; i < MV_SIZE; i++) {
if ( strmctx[i])
delete strmctx[i];
}
// delete widget;
if ( !(options & ge_mOptions_Embedded))
......@@ -196,6 +202,7 @@ XttMultiViewGtk::XttMultiViewGtk( GtkWidget *mv_parent_wid, void *mv_parent_ctx,
memset( seve, 0, sizeof(seve));
memset( trend, 0, sizeof(trend));
memset( sevhist, 0, sizeof(sevhist));
memset( strmctx, 0, sizeof(strmctx));
memset( comp_widget, 0, sizeof(comp_widget));
memset( exchange_widget, 0, sizeof(exchange_widget));
......@@ -635,6 +642,49 @@ XttMultiViewGtk::XttMultiViewGtk( GtkWidget *mv_parent_wid, void *mv_parent_ctx,
// "", NULL);
if ( mv.Action[i*rows+j].Options & pwr_mMultiViewElemOptionsMask_Exchangeable) {
exchange_widget[i*rows+j] = gtk_hbox_new( FALSE, 0);
gtk_box_pack_start( GTK_BOX(exchange_widget[i*rows+j]), GTK_WIDGET(comp_widget[i*rows + j]), TRUE, TRUE, 0);
gtk_box_pack_start( GTK_BOX(row_widget), GTK_WIDGET(exchange_widget[i*rows + j]), TRUE, TRUE, 0);
}
else
gtk_box_pack_start( GTK_BOX(row_widget), GTK_WIDGET(comp_widget[i*rows + j]), TRUE, TRUE, 0);
break;
}
case pwr_eMultiViewContentEnum_Video: {
pwr_sClass_XttVideo xttvideo;
pwr_tObjid objid;
pwr_tCid cid;
objid = mv.Action[i*rows+j].Object.Objid;
if ( cdh_ObjidIsNull(objid))
break;
lsts = gdh_GetObjectClass( objid, &cid);
if ( EVEN(lsts)) break;
if ( cid != pwr_cClass_XttVideo)
break;
pwr_tAttrRef aref = cdh_ObjidToAref( objid);
lsts = gdh_GetObjectInfoAttrref( &aref, (pwr_tAddress)&xttvideo, sizeof(xttvideo));
if (EVEN(lsts)) break;
unsigned int options = xttvideo.Options;
strmctx[i*rows + j] = new XttStreamGtk( toplevel, this, "No title",
xttvideo.URL,
mv.Action[i*rows+j].Width, mv.Action[i*rows+j].Height,
0, 0, 0, options, 1, sts);
strmctx[i*rows + j]->close_cb = multiview_strm_close_cb;
comp_widget[i*rows + j] = (GtkWidget *)strmctx[i*rows + j]->get_widget();
appl.insert( applist_eType_Stream, (void *)strmctx[i*rows + j], objid, xttvideo.Title,
xttvideo.URL);
if ( mv.Action[i*rows+j].Options & pwr_mMultiViewElemOptionsMask_Exchangeable) {
exchange_widget[i*rows+j] = gtk_hbox_new( FALSE, 0);
gtk_box_pack_start( GTK_BOX(exchange_widget[i*rows+j]), GTK_WIDGET(comp_widget[i*rows + j]), TRUE, TRUE, 0);
......@@ -1097,6 +1147,41 @@ int XttMultiViewGtk::set_subwindow_source( const char *name, char *source, char
mv.Action[i*rows+j].Object = object_aref;
}
case pwr_eMultiViewContentEnum_Video: {
pwr_sClass_XttVideo xttvideo;
pwr_tStatus lsts;
pwr_tAttrRef object_aref;
lsts = gdh_NameToAttrref( pwr_cNObjid, object, &object_aref);
if ( EVEN(lsts)) break;
lsts = gdh_GetObjectInfoAttrref( &object_aref, (pwr_tAddress)&xttvideo, sizeof(xttvideo));
if (EVEN(lsts)) break;
XttStreamGtk *ctx = new XttStreamGtk( toplevel, this, "No title",
xttvideo.URL, w, h, 0, 0,
0, xttvideo.Options, 1, &lsts);
GtkWidget *comp_w = (GtkWidget *)ctx->get_widget();
appl.remove( (void *)strmctx[i*rows+j]);
delete strmctx[i*rows+j];
gtk_widget_destroy( comp_widget[i*rows+j]);
gtk_box_pack_start( GTK_BOX(exchange_widget[i*rows+j]), GTK_WIDGET(comp_w), TRUE, TRUE, 0);
// gtk_container_add(GTK_CONTAINER(exchange_widget[i*rows + j]), comp_w);
gtk_widget_show_all( exchange_widget[i*rows + j]);
gtk_box_reorder_child( GTK_BOX(exchange_widget[i*rows+j]), comp_w, 0);
comp_widget[i*rows + j] = comp_w;
strmctx[i*rows+j] = ctx;
if ( insert)
recall_buffer[i*rows + j].insert( source, object);
appl.insert( applist_eType_Stream, (void *)strmctx[i*rows + j], object_aref.Objid, xttvideo.Title,
xttvideo.URL);
break;
}
default: ;
}
}
......
......@@ -51,6 +51,7 @@
class EvEveGtk;
class XttTrendGtk;
class XttSevHistGtk;
class XttStreamGtk;
class XttMultiViewGtk : public XttMultiView {
public:
......@@ -65,6 +66,7 @@ class XttMultiViewGtk : public XttMultiView {
EvEveGtk *seve[MV_SIZE];
XttTrendGtk *trend[MV_SIZE];
XttSevHistGtk *sevhist[MV_SIZE];
XttStreamGtk *strmctx[MV_SIZE];
CoWowFocusTimerGtk focustimer;
XttMultiViewGtk( GtkWidget *parent_wid, void *parent_ctx, const char *name, pwr_tAttrRef *aref,
......
......@@ -1076,12 +1076,23 @@ int OpGtk::configure( char *opplace_str)
sts = gdh_GetAttrRefTid( &opplace_p->FastAvail[i], &tid);
if ( EVEN(sts))continue;
if ( tid != pwr_cClass_XttGraph)
continue;
memset( &attrref, 0, sizeof(attrref));
sts = gdh_ClassAttrToAttrref( pwr_cClass_XttGraph, ".ButtonText", &attrref);
if ( EVEN(sts)) return sts;
switch ( tid) {
case pwr_cClass_XttGraph:
sts = gdh_ClassAttrToAttrref( pwr_cClass_XttGraph, ".ButtonText", &attrref);
if ( EVEN(sts)) return sts;
break;
case pwr_cClass_XttMultiView:
sts = gdh_ClassAttrToAttrref( pwr_cClass_XttMultiView, ".ButtonText", &attrref);
if ( EVEN(sts)) return sts;
break;
case pwr_cClass_XttVideo:
sts = gdh_ClassAttrToAttrref( pwr_cClass_XttVideo, ".ButtonText", &attrref);
if ( EVEN(sts)) return sts;
break;
default:
continue;
}
attrref = cdh_ArefAdd( &opplace_p->FastAvail[i], &attrref);
sts = gdh_GetObjectInfoAttrref( &attrref, (void *)button_title[i],
......
/*
* Proview Open Source Process Control.
* Copyright (C) 2005-2014 SSAB AB.
*
* This file is part of Proview.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proview. If not, see <http://www.gnu.org/licenses/>
*
* Linking Proview statically or dynamically with other modules is
* making a combined work based on Proview. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* In addition, as a special exception, the copyright holders of
* Proview give you permission to, from the build function in the
* Proview Configurator, combine Proview with modules generated by the
* Proview PLC Editor to a PLC program, regardless of the license
* terms of these modules. You may copy and distribute the resulting
* combined work under the terms of your choice, provided that every
* copy of the combined work is accompanied by a complete copy of
* the source code of Proview (the version used to produce the
* combined work), being distributed under the terms of the GNU
* General Public License plus this exception.
*/
#define PWRE_CONF_GST 1
#if defined PWRE_CONF_GST
#include <string.h>
#include "xtt_xnav.h"
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/interfaces/xoverlay.h>
#include <gdk/gdk.h>
#include <gdk/gdkx.h>
#include "pwr.h"
#include "co_dcli.h"
#include "xtt_stream_gtk.h"
#include "rt_xnav_msg.h"
#include "cow_wow_gtk.h"
int XttStreamGtk::gst_initialized = 0;
/* This function is called when the GUI toolkit creates the physical window that will hold the video.
* At this point we can retrieve its handler (which has a different meaning depending on the windowing system)
* and pass it to GStreamer through the XOverlay interface. */
void XttStreamGtk::realize_cb( GtkWidget *widget, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
GdkWindow *window = gtk_widget_get_window( widget);
guintptr window_handle;
if (!gdk_window_ensure_native (window))
g_error ("Couldn't create native window needed for GstXOverlay!");
/* Retrieve window handler from GDK */
window_handle = GDK_WINDOW_XID( window);
/* Pass it to playbin2, which implements XOverlay and will forward it to the video sink */
//gst_x_overlay_set_window_handle( GST_X_OVERLAY (strm->playbin2), window_handle);
gst_x_overlay_set_xwindow_id( GST_X_OVERLAY (strm->playbin2), window_handle);
}
/* This function is called when the PLAY button is clicked */
void XttStreamGtk::play_cb( GtkButton *button, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
gst_element_set_state( strm->playbin2, GST_STATE_PLAYING);
}
/* This function is called when the PAUSE button is clicked */
void XttStreamGtk::pause_cb( GtkButton *button, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
gst_element_set_state( strm->playbin2, GST_STATE_PAUSED);
}
/* This function is called when the STOP button is clicked */
void XttStreamGtk::stop_cb( GtkButton *button, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
gst_element_set_state( strm->playbin2, GST_STATE_READY);
}
/* This function is called when the main window is closed */
void XttStreamGtk::delete_event_cb (GtkWidget *widget, GdkEvent *event, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
stop_cb( NULL, strm);
if ( strm->close_cb)
(strm->close_cb)( strm->parent_ctx, strm);
delete strm;
}
/* This function is called everytime the video window needs to be redrawn (due to damage/exposure,
* rescaling, etc). GStreamer takes care of this in the PAUSED and PLAYING states, otherwise,
* we simply draw a black rectangle to avoid garbage showing up. */
gboolean XttStreamGtk::expose_cb( GtkWidget *widget, GdkEventExpose *event, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
if (strm->state < GST_STATE_PAUSED) {
GtkAllocation allocation;
GdkWindow *window = gtk_widget_get_window( widget);
cairo_t *cr;
/* Cairo is a 2D graphics library which we use here to clean the video window.
* It is used by GStreamer for other reasons, so it will always be available to us. */
gtk_widget_get_allocation( widget, &allocation);
cr = gdk_cairo_create (window);
cairo_set_source_rgb( cr, 0, 0, 0);
cairo_rectangle( cr, 0, 0, allocation.width, allocation.height);
cairo_fill( cr);
cairo_destroy( cr);
}
return FALSE;
}
/* This function is called when the slider changes its position. We perform a seek to the
* new position here. */
void XttStreamGtk::slider_cb( GtkRange *range, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
gdouble value = gtk_range_get_value( GTK_RANGE( strm->slider));
gst_element_seek_simple( strm->playbin2, GST_FORMAT_TIME, GstSeekFlags(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT),
(gint64)(value * GST_SECOND));
}
/* This function is called periodically to refresh the GUI */
void XttStreamGtk::refresh_ui( XttStreamGtk *strm)
{
GstFormat fmt = GST_FORMAT_TIME;
gint64 current = -1;
if ( !(strm->options & pwr_mVideoOptionsMask_ControlPanel &&
strm->options & pwr_mVideoOptionsMask_ProgressBar))
return;
/* We do not want to update anything unless we are in the PAUSED or PLAYING states */
if (strm->state < GST_STATE_PAUSED)
return;
/* If we didn't know it yet, query the stream duration */
if (!GST_CLOCK_TIME_IS_VALID( strm->duration)) {
if (!gst_element_query_duration( strm->playbin2, &fmt, &strm->duration)) {
g_printerr( "Could not query current duration.\n");
} else {
/* Set the range of the slider to the clip duration, in SECONDS */
if (GST_CLOCK_TIME_IS_VALID( strm->duration))
gtk_range_set_range( GTK_RANGE( strm->slider), 0, (gdouble)strm->duration / GST_SECOND);
}
}
if (gst_element_query_position( strm->playbin2, &fmt, &current)) {
/* Block the "value-changed" signal, so the slider_cb function is not called
* (which would trigger a seek the user has not requested) */
g_signal_handler_block( strm->slider, strm->slider_update_signal_id);
/* Set the position of the slider to the current pipeline positoin, in SECONDS */
gtk_range_set_value( GTK_RANGE( strm->slider), (gdouble)current / GST_SECOND);
/* Re-enable the signal */
g_signal_handler_unblock( strm->slider, strm->slider_update_signal_id);
}
}
void XttStreamGtk::refresh( void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
refresh_ui( strm);
strm->timerid->add( strm->scan_time, strm->refresh, data);
}
/* This function is called when new metadata is discovered in the stream */
void XttStreamGtk::tags_cb( GstElement *playbin2, gint stream, void *data)
{
/* We are possibly in a GStreamer working thread, so we notify the main
* thread of this event through a message in the bus */
gst_element_post_message( playbin2,
gst_message_new_application( GST_OBJECT( playbin2),
gst_structure_new( "tags-changed", NULL)));
}
/* This function is called when an error message is posted on the bus */
void XttStreamGtk::error_cb( GstBus *bus, GstMessage *msg, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
GError *err;
gchar *debug_info;
/* Print error details on the screen */
gst_message_parse_error( msg, &err, &debug_info);
printf( "Error received from element %s: %s\n", GST_OBJECT_NAME( msg->src), err->message);
printf( "Debugging information: %s\n", debug_info ? debug_info : "none");
g_clear_error( &err);
g_free( debug_info);
/* Set the pipeline to READY (which stops playback) */
gst_element_set_state( strm->playbin2, GST_STATE_READY);
}
/* This function is called when an End-Of-Stream message is posted on the bus.
* We just set the pipeline to READY (which stops playback) */
void XttStreamGtk::eos_cb( GstBus *bus, GstMessage *msg, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
g_print( "End-Of-Stream reached.\n");
gst_element_set_state( strm->playbin2, GST_STATE_READY);
}
/* This function is called when the pipeline changes states. We use it to
* keep track of the current state. */
void XttStreamGtk::state_changed_cb( GstBus *bus, GstMessage *msg, void *data)
{
XttStreamGtk *strm = (XttStreamGtk *)data;
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed( msg, &old_state, &new_state, &pending_state);
if (GST_MESSAGE_SRC( msg) == GST_OBJECT( strm->playbin2)) {
strm->state = new_state;
// g_print( "State set to %s\n", gst_element_state_get_name( new_state));
if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED) {
/* For extra responsiveness, we refresh the GUI as soon as we reach the PAUSED state */
strm->refresh_ui( strm);
}
}
}
/* Extract metadata from all the streams and write it to the text widget in the GUI */
static void analyze_streams( void *data) {
#if 0
XttStreamGtk *strm = (XttStreamGtk *)data;
gint i;
GstTagList *tags;
gchar *str, *total_str;
guint rate;
gint n_video, n_audio, n_text;
GtkTextBuffer *text;
/* Clean current contents of the widget */
text = gtk_text_view_get_buffer( GTK_TEXT_VIEW( strm->streams_list));
gtk_text_buffer_set_text( text, "", -1);
/* Read some properties */
g_object_get( strm->playbin2, "n-video", &n_video, NULL);
g_object_get( strm->playbin2, "n-audio", &n_audio, NULL);
g_object_get( strm->playbin2, "n-text", &n_text, NULL);
for (i = 0; i < n_video; i++) {
tags = NULL;
/* Retrieve the stream's video tags */
g_signal_emit_by_name( strm->playbin2, "get-video-tags", i, &tags);
if (tags) {
total_str = g_strdup_printf( "video stream %d:\n", i);
gtk_text_buffer_insert_at_cursor( text, total_str, -1);
g_free( total_str);
gst_tag_list_get_string( tags, GST_TAG_VIDEO_CODEC, &str);
str = 0;
total_str = g_strdup_printf( " codec: %s\n", str ? str : "unknown");
gtk_text_buffer_insert_at_cursor( text, total_str, -1);
g_free( total_str);
g_free( str);
gst_tag_list_free( tags);
}
}
for (i = 0; i < n_audio; i++) {
tags = NULL;
/* Retrieve the stream's audio tags */
g_signal_emit_by_name( strm->playbin2, "get-audio-tags", i, &tags);
if (tags) {
total_str = g_strdup_printf( "\naudio stream %d:\n", i);
gtk_text_buffer_insert_at_cursor( text, total_str, -1);
g_free( total_str);
if (gst_tag_list_get_string( tags, GST_TAG_AUDIO_CODEC, &str)) {
total_str = g_strdup_printf( " codec: %s\n", str);
gtk_text_buffer_insert_at_cursor( text, total_str, -1);
g_free( total_str);
g_free( str);
}
if (gst_tag_list_get_string( tags, GST_TAG_LANGUAGE_CODE, &str)) {
total_str = g_strdup_printf( " language: %s\n", str);
gtk_text_buffer_insert_at_cursor( text, total_str, -1);
g_free( total_str);
g_free( str);
}
if (gst_tag_list_get_uint( tags, GST_TAG_BITRATE, &rate)) {
total_str = g_strdup_printf( " bitrate: %d\n", rate);
gtk_text_buffer_insert_at_cursor( text, total_str, -1);
g_free( total_str);
}
gst_tag_list_free( tags);
}
}
for (i = 0; i < n_text; i++) {
tags = NULL;
/* Retrieve the stream's subtitle tags */
g_signal_emit_by_name( strm->playbin2, "get-text-tags", i, &tags);
if (tags) {
total_str = g_strdup_printf( "\nsubtitle stream %d:\n", i);
gtk_text_buffer_insert_at_cursor( text, total_str, -1);
g_free( total_str);
if (gst_tag_list_get_string( tags, GST_TAG_LANGUAGE_CODE, &str)) {
total_str = g_strdup_printf( " language: %s\n", str);
gtk_text_buffer_insert_at_cursor( text, total_str, -1);
g_free( total_str);
g_free( str);
}
gst_tag_list_free( tags);
}
}
#endif
}
/* This function is called when an "application" message is posted on the bus.
* Here we retrieve the message posted by the tags_cb callback */
void XttStreamGtk::application_cb( GstBus *bus, GstMessage *msg, void *data)
{
if (g_strcmp0( gst_structure_get_name( msg->structure), "tags-changed") == 0) {
/* If the message is the "tags-changed" (only one we are currently issuing), update
* the stream info GUI */
analyze_streams( data);
}
}
XttStreamGtk::XttStreamGtk( GtkWidget *st_parent_wid, void *st_parent_ctx, const char *name, const char *st_uri,
int width, int height, int x, int y, double scan_time,
unsigned int st_options, int st_embedded, pwr_tStatus *sts) :
XttStream( st_parent_ctx, name, st_uri, width, height, x, y, scan_time, st_options, st_embedded),
parent_wid(st_parent_wid)
{
GstStateChangeReturn ret;
GstBus *bus;
pwr_tFileName fname;
if ( !gst_initialized) {
// Initialize gstreamer
int argc = 0;
char **argv;
gst_init( &argc, &argv);
gst_initialized = 1;
}
if( width == 0 || height == 0) {
width = 640;
height = 480;
}
duration = GST_CLOCK_TIME_NONE;
/* Create the elements */
playbin2 = gst_element_factory_make( "playbin2", "playbin2");
if (!playbin2) {
g_printerr( "Not all elements could be created.\n");
*sts = 0;
return;
}
/* Set the URI to play, eg "http://192.168.67.248/mjpg/video.mjpg" */
g_object_set( playbin2, "uri", uri, NULL);
/* Connect to interesting signals in playbin2 */
g_signal_connect( G_OBJECT( playbin2), "video-tags-changed",( GCallback) tags_cb, this);
g_signal_connect( G_OBJECT( playbin2), "audio-tags-changed",( GCallback) tags_cb, this);
g_signal_connect( G_OBJECT( playbin2), "text-tags-changed",( GCallback) tags_cb, this);
if ( !embedded) {
toplevel = gtk_window_new (GTK_WINDOW_TOPLEVEL);
g_signal_connect( G_OBJECT(toplevel), "delete-event", G_CALLBACK(delete_event_cb), this);
char *titleutf8 = g_convert( name, -1, "UTF-8", "ISO8859-1", NULL, NULL, NULL);
gtk_window_set_title( GTK_WINDOW(toplevel), titleutf8);
g_free( titleutf8);
CoWowGtk::SetWindowIcon( toplevel);
}
else
toplevel = parent_wid;
video_form = gtk_drawing_area_new();
gtk_widget_set_double_buffered( video_form, FALSE);
g_signal_connect( video_form, "realize", G_CALLBACK( realize_cb), this);
g_signal_connect( video_form, "expose_event", G_CALLBACK( expose_cb), this);
// GtkWidget *controls;
GtkWidget *hbox = gtk_hbox_new( FALSE, 0);
if ( options & pwr_mVideoOptionsMask_ControlPanel) {
GtkToolbar *controlbuttons;
controlbuttons = (GtkToolbar *) g_object_new(GTK_TYPE_TOOLBAR, NULL);
GtkWidget *play_button = gtk_button_new();
dcli_translate_filename( fname, "$pwr_exe/xtt_play.png");
gtk_container_add( GTK_CONTAINER(play_button),
gtk_image_new_from_file( fname));
g_signal_connect( G_OBJECT( play_button), "clicked", G_CALLBACK( play_cb), this);
gtk_toolbar_append_widget( controlbuttons, play_button,CoWowGtk::translate_utf8("Play"), "");
GtkWidget *pause_button = gtk_button_new();
dcli_translate_filename( fname, "$pwr_exe/xtt_pause.png");
gtk_container_add( GTK_CONTAINER(pause_button),
gtk_image_new_from_file( fname));
g_signal_connect( G_OBJECT( pause_button), "clicked", G_CALLBACK( pause_cb), this);
gtk_toolbar_append_widget( controlbuttons, pause_button,CoWowGtk::translate_utf8("Pause"), "");
GtkWidget *stop_button = gtk_button_new();
dcli_translate_filename( fname, "$pwr_exe/xtt_stop.png");
gtk_container_add( GTK_CONTAINER(stop_button),
gtk_image_new_from_file( fname));
g_signal_connect( G_OBJECT( stop_button), "clicked", G_CALLBACK( stop_cb), this);
gtk_toolbar_append_widget( controlbuttons, stop_button,CoWowGtk::translate_utf8("Stop"), "");
gtk_box_pack_start( GTK_BOX( hbox), GTK_WIDGET(controlbuttons), FALSE, FALSE, 2);
if ( options & pwr_mVideoOptionsMask_ProgressBar) {
slider = gtk_hscale_new_with_range( 0, 100, 1);
gtk_scale_set_draw_value( GTK_SCALE( slider), 0);
slider_update_signal_id = g_signal_connect( G_OBJECT( slider), "value-changed", G_CALLBACK( slider_cb), this);
gtk_box_pack_start( GTK_BOX( hbox), slider, TRUE, TRUE, 2);
}
}
main_box = gtk_vbox_new( FALSE, 0);
gtk_box_pack_start( GTK_BOX( main_box), video_form, TRUE, TRUE, 0);
if ( options & pwr_mVideoOptionsMask_ControlPanel)
gtk_box_pack_start( GTK_BOX( main_box), GTK_WIDGET(hbox), FALSE, FALSE, 0);
if ( !embedded) {
gtk_container_add( GTK_CONTAINER( toplevel), main_box);
gtk_window_set_default_size( GTK_WINDOW( toplevel), width, height);
gtk_widget_show_all( toplevel);
if ( options & pwr_mVideoOptionsMask_FullScreen)
gtk_window_fullscreen( GTK_WINDOW(toplevel));
else if ( options & pwr_mVideoOptionsMask_Maximize)
gtk_window_maximize( GTK_WINDOW(toplevel)); // TODO
else if ( options & pwr_mVideoOptionsMask_FullMaximize)
gtk_window_maximize( GTK_WINDOW(toplevel));
else if ( options & pwr_mVideoOptionsMask_Iconify)
gtk_window_iconify( GTK_WINDOW(toplevel));
}
else {
gtk_widget_set_size_request( main_box, width, height);
}
/* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
bus = gst_element_get_bus( playbin2);
gst_bus_add_signal_watch( bus);
g_signal_connect( G_OBJECT( bus), "message::error", (GCallback)error_cb, this);
g_signal_connect( G_OBJECT( bus), "message::eos", (GCallback)eos_cb, this);
g_signal_connect( G_OBJECT( bus), "message::state-changed", (GCallback)state_changed_cb, this);
g_signal_connect( G_OBJECT( bus), "message::application", (GCallback)application_cb, this);
gst_object_unref( bus);
/* Start playing */
ret = gst_element_set_state( playbin2, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
g_printerr( "Unable to set the pipeline to the playing state.\n");
gst_object_unref( playbin2);
*sts = 0;
return;
}
wow = new CoWowGtk( toplevel);
timerid = wow->timer_new();
timerid->add( scan_time, refresh, this);
*sts = XNAV__SUCCESS;
}
XttStreamGtk::~XttStreamGtk()
{
timerid->remove();
gst_element_set_state( playbin2, GST_STATE_NULL);
gst_object_unref( playbin2);
if ( !embedded)
gtk_widget_destroy( toplevel);
}
void XttStreamGtk::pop()
{
gtk_window_present( GTK_WINDOW(toplevel));
}
void XttStreamGtk::set_size( int width, int height)
{
gtk_window_resize( GTK_WINDOW(toplevel), width, height);
}
#if 0
int main(int argc, char *argv[]) {
pwr_tStatus sts;
/* Initialize GTK */
gtk_init( &argc, &argv);
/* Initialize GStreamer */
gst_init( &argc, &argv);
XttStreamGtk *strm = new XttStreamGtk( 0, 0, "Some video", "http://192.168.67.248/mjpg/video.mjpg", 1, 0, 0, 0, 0, 1, 0, &sts);
/* Start the GTK main loop. We will not regain control until gtk_main_quit is called. */
gtk_main();
delete strm;
/* Free resources */
return 0;
}
#endif
#else
// gstreamer not installed
#include <gtk/gtk.h>
#include "xtt_stream_gtk.h"
XttStreamGtk::XttStreamGtk( GtkWidget *parent_wid, void *parent_ctx, const char *name, const char *uri,
int width, int height,
int x, int y, double scan_time, unsigned int options) {}
XttStreamGtk::~XttStreamGtk() {}
#endif
/*
* Proview Open Source Process Control.
* Copyright (C) 2005-2014 SSAB AB.
*
* This file is part of Proview.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proview. If not, see <http://www.gnu.org/licenses/>
*
* Linking Proview statically or dynamically with other modules is
* making a combined work based on Proview. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* In addition, as a special exception, the copyright holders of
* Proview give you permission to, from the build function in the
* Proview Configurator, combine Proview with modules generated by the
* Proview PLC Editor to a PLC program, regardless of the license
* terms of these modules. You may copy and distribute the resulting
* combined work under the terms of your choice, provided that every
* copy of the combined work is accompanied by a complete copy of
* the source code of Proview (the version used to produce the
* combined work), being distributed under the terms of the GNU
* General Public License plus this exception.
*/
#ifndef xtt_stream_gtk_h
#define xtt_stream_gtk_h
#ifndef xtt_stream_h
# include "xtt_stream.h"
#endif
#ifndef cow_wow_gtk_h
# include "cow_wow_gtk.h"
#endif
#if defined PWRE_CONF_GST
#include <gst/gst.h>
class XttStreamGtk : public XttStream {
public:
GstElement *playbin2; /* Our one and only pipeline */
gulong slider_update_signal_id; /* Signal ID for the slider update signal */
GstState state; /* Current state of the pipeline */
gint64 duration; /* Duration of the clip, in nanoseconds */
GtkWidget *slider; /* Slider widget to keep track of current position */
GtkWidget *parent_wid;
GtkWidget *toplevel;
GtkWidget *video_form;
GtkWidget *main_box;
// CoWowFocusTimerGtk focustimer;
static int gst_initialized;
XttStreamGtk( GtkWidget *parent_wid, void *parent_ctx, const char *name, const char *uri,
int width, int height, int x, int y,
double scan_time, unsigned int options, int embedded, pwr_tStatus *sts);
~XttStreamGtk();
void pop();
void set_size( int width, int height);
void *get_widget() { return main_box;}
static void refresh( void *data);
static void refresh_ui( XttStreamGtk *strm);
static void activate_exit( GtkWidget *w, gpointer data);
static void realize_cb( GtkWidget *widget, void *data);
static void play_cb( GtkButton *button, void *data);
static void pause_cb( GtkButton *button, void *data);
static void stop_cb( GtkButton *button, void *data);
static void delete_event_cb (GtkWidget *widget, GdkEvent *event, void *data);
static gboolean expose_cb( GtkWidget *widget, GdkEventExpose *event, void *data);
static void slider_cb( GtkRange *range, void *data);
static void tags_cb( GstElement *playbin2, gint stream, void *data);
static void error_cb( GstBus *bus, GstMessage *msg, void *data);
static void eos_cb( GstBus *bus, GstMessage *msg, void *data);
static void state_changed_cb( GstBus *bus, GstMessage *msg, void *data);
static void application_cb( GstBus *bus, GstMessage *msg, void *data);
};
#else
class XttStreamGtk : public XttStream {
public:
XttStreamGtk( GtkWidget *parent_wid, void *parent_ctx, const char *name, const char *uri,
int width, int height, int x, int y,
double scan_time, unsigned int options, int embedded, pwr_tStatus *sts);
~XttStreamGtk();
void pop() {}
void set_size( int width, int height) {}
void *get_widget() { return 0;}
};
#endif
#endif
......@@ -77,6 +77,7 @@ typedef void *Widget;
#include "xtt_xcolwind_gtk.h"
#include "xtt_ge_gtk.h"
#include "xtt_multiview_gtk.h"
#include "xtt_stream_gtk.h"
#include "xtt_block_gtk.h"
#include "xtt_trend_gtk.h"
#include "xtt_sevhist_gtk.h"
......@@ -345,6 +346,14 @@ XttMultiView *XNavGtk::multiview_new( const char *name, pwr_tAttrRef *aref,
get_current_objects_cb, is_authorized_cb);
}
XttStream *XNavGtk::stream_new( const char *name, const char *uri,
int width, int height, int x, int y, double scan_time,
unsigned int options, int embedded, pwr_tStatus *sts)
{
return new XttStreamGtk( parent_wid, this, name, uri, width, height, x, y,
scan_time, options, embedded, sts);
}
GeCurve *XNavGtk::gecurve_new( char *name, char *filename, GeCurveData *data,
int pos_right, unsigned int options)
{
......
......@@ -101,6 +101,9 @@ class XNavGtk : public XNav {
int (*command_cb) (void *, char *, void *),
int (*get_current_objects_cb) (void *, pwr_sAttrRef **, int **),
int (*is_authorized_cb) (void *, unsigned int));
XttStream *stream_new( const char *name, const char *uri,
int width, int height, int x, int y, double scan_time,
unsigned int options, int embedded, pwr_tStatus *sts);
GeCurve *gecurve_new( char *name, char *filename, GeCurveData *data,
int pos_right, unsigned int options);
XttFileview *fileview_new( pwr_tOid oid, char *title, char *dir, char *pattern,
......
......@@ -48,7 +48,8 @@ typedef enum {
applist_eType_Crossref,
applist_eType_Hist,
applist_eType_Fast,
applist_eType_MultiView
applist_eType_MultiView,
applist_eType_Stream
} applist_eType;
......
......@@ -908,6 +908,13 @@ static pwr_tStatus OpenGraph( xmenu_sMenuCall *ip)
sts = ((XNav *)ip->EditorContext)->command( cmd);
return XNAV__SUCCESS;
}
else if ( classid == pwr_cClass_XttVideo) {
sts = gdh_AttrrefToName( objar, name, sizeof(name), cdh_mNName);
strcpy( cmd, "ope vide/obj=");
strcat( cmd, name);
sts = ((XNav *)ip->EditorContext)->command( cmd);
return XNAV__SUCCESS;
}
while( ODD(sts)) {
sts = gdh_AttrrefToName( &aref, name, sizeof(name),
......@@ -935,6 +942,13 @@ static pwr_tStatus OpenGraph( xmenu_sMenuCall *ip)
sts = ((XNav *)ip->EditorContext)->command( cmd);
break;
}
else if ( classid == pwr_cClass_XttVideo) {
sts = gdh_AttrrefToName( &defgraph, name, sizeof(name), cdh_mNName);
strcpy( cmd, "ope vide/obj=");
strcat( cmd, name);
sts = ((XNav *)ip->EditorContext)->command( cmd);
break;
}
}
if ( aref.Flags.b.Object) {
......@@ -973,7 +987,8 @@ static pwr_tStatus OpenGraphFilter( xmenu_sMenuCall *ip)
if ( EVEN(sts)) return sts;
if ( classid == pwr_cClass_XttGraph ||
classid == pwr_cClass_XttMultiView)
classid == pwr_cClass_XttMultiView ||
classid == pwr_cClass_XttVideo)
return XNAV__SUCCESS;
while( ODD(sts)) {
......@@ -1008,6 +1023,8 @@ static pwr_tStatus OpenGraphFilter( xmenu_sMenuCall *ip)
}
else if ( classid == pwr_cClass_XttMultiView)
return XNAV__SUCCESS;
else if ( classid == pwr_cClass_XttVideo)
return XNAV__SUCCESS;
}
if ( aref.Flags.b.Object) {
......
......@@ -230,6 +230,10 @@ int XttMultiView::multiview_sevhist_get_select_cb( void *ctx, pwr_tOid *oid, cha
return 0;
}
void XttMultiView::multiview_strm_close_cb( void *ctx, XttStream *strm)
{
}
void XttMultiView::message_cb( void *ctx, char severity, const char *msg)
{
((XttMultiView *)ctx)->message( severity, msg);
......
......@@ -54,6 +54,7 @@
class Graph;
class XNav;
class XttTrend;
class XttStream;
class MVRecall {
public:
......@@ -140,6 +141,7 @@ class XttMultiView {
static void multiview_trend_command_cb( void *ctx, const char *cmd);
static void multiview_trend_help_cb( void *ctx, const char *key);
static int multiview_sevhist_get_select_cb( void *ctx, pwr_tOid *oid, char *aname, char *oname);
static void multiview_strm_close_cb( void *ctx, XttStream *strm);
static void message_cb( void *ctx, char severity, const char *msg);
static void eventlog_enable( int enable);
};
......
......@@ -99,12 +99,31 @@ int Op::appl_action( int idx)
pwr_tCmd cmd;
pwr_tAName name;
int sts;
pwr_tCid cid;
if ( command_cb) {
sts = gdh_AttrrefToName( &button_aref[idx], name, sizeof(name), cdh_mName_volumeStrict);
strcpy( cmd, "ope gra/obj=");
strcat( cmd, name);
if ( EVEN(sts)) return sts;
sts = gdh_GetAttrRefTid( &button_aref[idx], &cid);
if ( EVEN(sts)) return sts;
switch ( cid) {
case pwr_cClass_XttGraph:
strcpy( cmd, "ope gra/obj=");
strcat( cmd, name);
break;
case pwr_cClass_XttMultiView:
strcpy( cmd, "ope mult ");
strcat( cmd, name);
break;
case pwr_cClass_XttVideo:
strcpy( cmd, "ope vide/obj=");
strcat( cmd, name);
break;
default:
return 0;
}
command_cb( parent_ctx, cmd);
}
return XNAV__SUCCESS;
......
/*
* Proview Open Source Process Control.
* Copyright (C) 2005-2014 SSAB AB.
*
* This file is part of Proview.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proview. If not, see <http://www.gnu.org/licenses/>
*
* Linking Proview statically or dynamically with other modules is
* making a combined work based on Proview. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* In addition, as a special exception, the copyright holders of
* Proview give you permission to, from the build function in the
* Proview Configurator, combine Proview with modules generated by the
* Proview PLC Editor to a PLC program, regardless of the license
* terms of these modules. You may copy and distribute the resulting
* combined work under the terms of your choice, provided that every
* copy of the combined work is accompanied by a complete copy of
* the source code of Proview (the version used to produce the
* combined work), being distributed under the terms of the GNU
* General Public License plus this exception.
*/
#ifndef xtt_stream_h
#define xtt_stream_h
#ifndef pwr_h
# include "pwr.h"
#endif
class CoWow;
class CoWowTimer;
class XttStream {
public:
void *parent_ctx;
unsigned int options;
int embedded;
pwr_tURL uri;
CoWowTimer *timerid;
CoWow *wow;
int scan_time;
void (*close_cb)( void *, XttStream *);
XttStream( void *st_parent_ctx, const char *name, const char *st_uri,
int width, int height, int x, int y,
double st_scan_time, unsigned int st_options, int st_embedded) :
parent_ctx(st_parent_ctx), options(st_options), embedded(st_embedded), timerid(0), close_cb(0) {
strncpy( uri, st_uri, sizeof(uri));
if ( st_scan_time < 0.02)
scan_time = 1000;
else
scan_time = 1000 * st_scan_time;
}
virtual ~XttStream() {}
virtual void pop() {}
virtual void set_size( int width, int height) {}
virtual void *get_widget() { return 0;}
};
#endif
......@@ -4352,6 +4352,12 @@ void XNav::appl_startup()
}
command( cmd);
break;
case pwr_cClass_XttVideo:
strcpy( cmd, "ope vid/obj=");
strcat( cmd, name);
command( cmd);
break;
default: ;
}
}
......
......@@ -137,6 +137,7 @@ class XttGe;
class RtTrace;
class XttFileview;
class CoLogin;
class XttStream;
typedef enum {
xnav_mOpen_All = ~0,
......@@ -380,6 +381,9 @@ class XNav {
int (*command_cb) (void *, char *, void *),
int (*get_current_objects_cb) (void *, pwr_sAttrRef **, int **),
int (*is_authorized_cb) (void *, unsigned int)) {return 0;}
virtual XttStream *stream_new( const char *name, const char *uri,
int width, int height, int x, int y, double scan_time,
unsigned int options, int embedded, pwr_tStatus *sts) {return 0;}
virtual GeCurve *gecurve_new( char *name, char *filename, GeCurveData *data,
int pos_right, unsigned int options) {return 0;}
virtual XttFileview *fileview_new( pwr_tOid oid, char *title, char *dir, char *pattern,
......
......@@ -98,6 +98,7 @@
#include "xtt_hist.h"
#include "xtt_fileview.h"
#include "xtt_log.h"
#include "xtt_stream.h"
class xnav_file {
public:
......@@ -166,6 +167,7 @@ static int xnav_multiview_command_cb( void *gectx, char *command, void *caller);
static int xnav_ge_command_cb( void *gectx, char *command, void *caller);
static void xnav_ge_close_cb( void *xnav, void *gectx);
static void xnav_multiview_close_cb( void *xnav, void *mvctx);
static void xnav_stream_close_cb( void *xnav, XttStream *strmctx);
//new code by Jonas Nylund 030131
static void xnav_hist_close_cb( void *ctx);
//end new code by Jonas Nylund 030131
......@@ -276,7 +278,7 @@ dcli_tCmdTable xnav_command_table[] = {
"/PINSTANCE", "/BYPASS",
"/CLOSEBUTTON", "/TARGET", "/TRIGGER", "/TYPE", "/FTYPE",
"/FULLSCREEN", "/MAXIMIZE", "/FULLMAXIMIZE", "/ICONIFY", "/HIDE",
"/XPOSITION", "/YPOSITION", "/X0", "/Y0", "/X1", "/Y1", ""}
"/XPOSITION", "/YPOSITION", "/X0", "/Y0", "/X1", "/Y1", "/URL", "/CONTINOUS", ""}
},
{
"CLOSE",
......@@ -3248,13 +3250,18 @@ static int xnav_open_func( void *client_data,
if ( EVEN( dcli_get_qualifier( "/NAME", name_str, sizeof(name_str)))) {
if ( instance_p) {
pwr_sAttrRef aref;
char *s;
// Use object name as title
if ( (s = strchr( instance_p, ',')))
*s = 0;
sts = gdh_NameToAttrref( pwr_cNObjid, instance_p, &aref);
if ( EVEN(sts)) {
xnav->message('E',"Instance object not found");
return XNAV__HOLDCOMMAND;
}
if ( s)
*s = ',';
sts = gdh_AttrrefToName( &aref, name_str, sizeof(name_str),
cdh_mName_pathStrict);
if ( EVEN(sts)) return sts;
......@@ -3496,6 +3503,151 @@ static int xnav_open_func( void *client_data,
}
return XNAV__SUCCESS;
}
else if ( cdh_NoCaseStrncmp( arg1_str, "VIDEO", strlen( arg1_str)) == 0) {
char tmp_str[80];
int width, height;
int x, y;
unsigned int options = 0;
pwr_tString80 name_str;
int nr;
pwr_tStatus sts;
pwr_tURL url_str;
pwr_tAName object_str;
// Command is "OPEN VIDEO"
if ( ODD( dcli_get_qualifier( "/OBJECT", object_str, sizeof(object_str)))) {
// XttVideo object supplied, fetch data from object
pwr_tOName xttvideo_name;
pwr_sClass_XttVideo xttvideo;
pwr_tObjid objid;
pwr_tCid cid;
xnav_replace_node_str( xttvideo_name, object_str);
sts = gdh_NameToObjid( xttvideo_name, &objid);
if (EVEN(sts)) {
xnav->message('E', "Object not found");
return XNAV__HOLDCOMMAND;
}
sts = gdh_GetObjectClass( objid, &cid);
if ( EVEN(sts)) return sts;
if ( cid != pwr_cClass_XttVideo) {
xnav->message('E', "Error in object class");
return XNAV__HOLDCOMMAND;
}
pwr_tAttrRef aref = cdh_ObjidToAref( objid);
sts = gdh_GetObjectInfoAttrref( &aref, (pwr_tAddress)&xttvideo, sizeof(xttvideo));
if (EVEN(sts)) return sts;
strncpy( name_str, xttvideo.Title, sizeof(name_str));
strncpy( url_str, xttvideo.URL, sizeof(url_str));
width = xttvideo.Width;
height = xttvideo.Height;
x = xttvideo.X;
y = xttvideo.Y;
options = xttvideo.Options;
XttStream *strmctx;
if ( xnav->appl.find( applist_eType_Stream, objid, (void **) &strmctx)) {
strmctx->pop();
}
else {
strmctx = xnav->stream_new( name_str, url_str, width, height, x, y, 0, options, 0, &sts);
if ( EVEN(sts)) {
xnav->message(' ', XNav::get_message(sts));
return sts;
}
strmctx->close_cb = xnav_stream_close_cb;
xnav->appl.insert( applist_eType_Stream, (void *)strmctx, objid, name_str, url_str);
}
}
else {
/* Get the name qualifier */
if ( EVEN( dcli_get_qualifier( "/NAME", name_str, sizeof(name_str))))
strcpy( name_str, "");
if ( EVEN( dcli_get_qualifier( "/URL", url_str, sizeof(url_str)))) {
xnav->message('E', "Url is missing");
return XNAV__SUCCESS;
}
if ( ODD( dcli_get_qualifier( "/FULLSCREEN", 0, 0)))
options |= pwr_mVideoOptionsMask_FullScreen;
if ( ODD( dcli_get_qualifier( "/MAXIMIZE", 0, 0)))
options |= pwr_mVideoOptionsMask_Maximize;
if ( ODD( dcli_get_qualifier( "/FULLMAXIMIZE", 0, 0)))
options |= pwr_mVideoOptionsMask_FullMaximize;
if ( ODD( dcli_get_qualifier( "/ICONIFY", 0, 0)))
options |= pwr_mVideoOptionsMask_Iconify;
if ( ODD( dcli_get_qualifier( "/CONTROLPANEL", 0, 0)))
options |= pwr_mVideoOptionsMask_ControlPanel;
if ( ODD( dcli_get_qualifier( "/PROGRESSBAR", 0, 0)))
options |= pwr_mVideoOptionsMask_ProgressBar;
if ( ODD( dcli_get_qualifier( "/WIDTH", tmp_str, sizeof(tmp_str)))) {
nr = sscanf( tmp_str, "%d", &width);
if ( nr != 1) {
xnav->message('E', "Syntax error in width");
return XNAV__HOLDCOMMAND;
}
}
else
width = 0;
if ( ODD( dcli_get_qualifier( "/HEIGHT", tmp_str, sizeof(tmp_str)))) {
nr = sscanf( tmp_str, "%d", &height);
if ( nr != 1) {
xnav->message('E', "Syntax error in height");
return XNAV__HOLDCOMMAND;
}
}
else
height = 0;
if ( ODD( dcli_get_qualifier( "/XPOSITION", tmp_str, sizeof(tmp_str)))) {
nr = sscanf( tmp_str, "%d", &x);
if ( nr != 1) {
xnav->message('E', "Syntax error in x coordinate");
return XNAV__HOLDCOMMAND;
}
}
else
x = 0;
if ( ODD( dcli_get_qualifier( "/YPOSITION", tmp_str, sizeof(tmp_str)))) {
nr = sscanf( tmp_str, "%d", &y);
if ( nr != 1) {
xnav->message('E', "Syntax error in y coordinate");
return XNAV__HOLDCOMMAND;
}
}
else
y = 0;
XttStream *strmctx;
if ( xnav->appl.find( applist_eType_Stream, name_str, url_str, (void **) &strmctx)) {
strmctx->pop();
}
else {
strmctx = xnav->stream_new( name_str, url_str, width, height, x, y, 0, options, 0, &sts);
if ( EVEN(sts)) {
xnav->message(' ', XNav::get_message(sts));
return sts;
}
strmctx->close_cb = xnav_stream_close_cb;
xnav->appl.insert( applist_eType_Stream, (void *)strmctx, pwr_cNOid, name_str, url_str);
}
}
return XNAV__SUCCESS;
}
else if ( cdh_NoCaseStrncmp( arg1_str, "TRACE", strlen( arg1_str)) == 0)
{
pwr_tOName name_str;
......@@ -5199,6 +5351,13 @@ static void xnav_multiview_close_cb( void *nav, void *ctx)
xnav->appl.remove( (void *)ctx);
}
static void xnav_stream_close_cb( void *nav, XttStream *ctx)
{
XNav *xnav = (XNav *)nav;
xnav->appl.remove( (void *)ctx);
}
//new code Jonas Nylund 030131
static void xnav_hist_close_cb( void *ctx)
{
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment