Merge pull request #2318 from FinnStutzenstein/projectorSize
Control the resolution of the projector
This commit is contained in:
commit
c87111e81d
@ -4,13 +4,15 @@ from django.utils.translation import ugettext as _
|
||||
from .exceptions import ConfigError, ConfigNotFound
|
||||
from .models import ConfigStore
|
||||
|
||||
# remove resolution when changing to multiprojector
|
||||
INPUT_TYPE_MAPPING = {
|
||||
'string': str,
|
||||
'text': str,
|
||||
'integer': int,
|
||||
'boolean': bool,
|
||||
'choice': str,
|
||||
'colorpicker': str}
|
||||
'colorpicker': str,
|
||||
'resolution': dict}
|
||||
|
||||
|
||||
class ConfigHandler:
|
||||
@ -86,6 +88,16 @@ class ConfigHandler:
|
||||
except DjangoValidationError as e:
|
||||
raise ConfigError(e.messages[0])
|
||||
|
||||
# remove this block when changing to multiprojector
|
||||
if config_variable.input_type == 'resolution':
|
||||
if value.get('width') is None or value.get('height') is None:
|
||||
raise ConfigError(_('A width and a height have to be given.'))
|
||||
if not isinstance(value['width'], int) or not isinstance(value['height'], int):
|
||||
raise ConfigError(_('Data has to be integers.'))
|
||||
if (value['width'] < 800 or value['width'] > 3840 or
|
||||
value['height'] < 600 or value['height'] > 2160):
|
||||
raise ConfigError(_('The Resolution have to be between 800x600 and 3840x2160.'))
|
||||
|
||||
# Save the new value to the database.
|
||||
ConfigStore.objects.update_or_create(key=key, defaults={'value': value})
|
||||
|
||||
|
@ -155,3 +155,12 @@ def get_config_variables():
|
||||
label='Default countdown',
|
||||
weight=185,
|
||||
group='Projector')
|
||||
|
||||
# set the resolution for one projector. It can be removed with the multiprojector feature.
|
||||
yield ConfigVariable(
|
||||
name='projector_resolution',
|
||||
default_value={'width': 1024, 'height': 768},
|
||||
input_type='resolution',
|
||||
label='Projector Resolution',
|
||||
weight=200,
|
||||
group='Projector')
|
||||
|
25
openslides/core/migrations/0004_projector_resolution.py
Normal file
25
openslides/core/migrations/0004_projector_resolution.py
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.9 on 2016-08-25 11:56
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_auto_20160815_1911'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='projector',
|
||||
name='height',
|
||||
field=models.PositiveIntegerField(default=768),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projector',
|
||||
name='width',
|
||||
field=models.PositiveIntegerField(default=1024),
|
||||
),
|
||||
]
|
@ -66,6 +66,11 @@ class Projector(RESTModelMixin, models.Model):
|
||||
|
||||
scroll = models.IntegerField(default=0)
|
||||
|
||||
# currently unused, but important for the multiprojector.
|
||||
width = models.PositiveIntegerField(default=1024)
|
||||
|
||||
height = models.PositiveIntegerField(default=768)
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Contains general permissions that can not be placed in a specific app.
|
||||
|
@ -30,7 +30,7 @@ class ProjectorSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Projector
|
||||
fields = ('id', 'config', 'elements', 'scale', 'scroll', )
|
||||
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'width', 'height',)
|
||||
|
||||
|
||||
class CustomSlideSerializer(ModelSerializer):
|
||||
|
@ -322,6 +322,14 @@ img {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* .resolution can be removed with the multiprojector, but maybe
|
||||
* it could be reused for the settings in the projectormanage-view */
|
||||
.col1 .input-group .resolution {
|
||||
float: none;
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
/* Toolbar to save motion in inline editing mode */
|
||||
.motion-save-toolbar {
|
||||
@ -633,23 +641,14 @@ img {
|
||||
|
||||
/* iframe for live view */
|
||||
.col2 #iframe {
|
||||
width: 1024px;
|
||||
height: 768px;
|
||||
-moz-transform-origin: 0 0;
|
||||
-webkit-transform-origin: 0 0;
|
||||
-o-transform-origin: 0 0;
|
||||
transform-origin: 0 0 0;
|
||||
-moz-transform: scale(0.25);
|
||||
-webkit-transform: scale(0.25);
|
||||
-o-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
/* IE8+ - must be on one line, unfortunately */
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.25, M12=0, M21=0, M22=0.25, SizingMethod='auto expand')";
|
||||
}
|
||||
|
||||
.col2 #iframewrapper {
|
||||
width: 256px;
|
||||
height: 192px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid #D5D5D5;
|
||||
@ -658,7 +657,6 @@ img {
|
||||
|
||||
.col2 #iframeoverlay {
|
||||
width: 256px;
|
||||
height: 192px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
@ -666,9 +664,7 @@ img {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
/** Footer **/
|
||||
|
||||
#footer {
|
||||
float: left;
|
||||
height: 50px;
|
||||
|
@ -11,6 +11,44 @@ body{
|
||||
color: #222;
|
||||
}
|
||||
|
||||
/*** ProjectorContainer ***/
|
||||
.pContainer {
|
||||
background-color: #222;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.pContainer > div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pContainer #iframe {
|
||||
-moz-transform-origin: 0 0;
|
||||
-webkit-transform-origin: 0 0;
|
||||
-o-transform-origin: 0 0;
|
||||
transform-origin: 0 0 0;
|
||||
}
|
||||
|
||||
.pContainer #iframewrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.pContainer #iframeoverlay {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/*** HEADER ***/
|
||||
#header {
|
||||
box-shadow: 0 0 7px rgba(0,0,0,0.6);
|
||||
|
@ -286,7 +286,6 @@ angular.module('OpenSlidesApp.core', [
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
.factory('jsDataModel', [
|
||||
'$http',
|
||||
'Projector',
|
||||
|
@ -57,6 +57,65 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
||||
}
|
||||
])
|
||||
|
||||
// Projector Container Controller
|
||||
.controller('ProjectorContainerCtrl', [
|
||||
'$scope',
|
||||
'Config',
|
||||
function($scope, Config) {
|
||||
// watch for changes in Config
|
||||
var last_conf;
|
||||
$scope.$watch(function () {
|
||||
return Config.lastModified();
|
||||
}, function () {
|
||||
var conf = Config.get('projector_resolution').value;
|
||||
// With multiprojector, get the resolution from Prjector.get(pk).{width; height}
|
||||
if(!last_conf || last_conf.width != conf.width || last-conf.height != conf.height) {
|
||||
last_conf = conf;
|
||||
$scope.projectorWidth = conf.width;
|
||||
$scope.projectorHeight = conf.height;
|
||||
$scope.recalculateIframe();
|
||||
}
|
||||
});
|
||||
|
||||
// recalculate the actual Iframesize and scale
|
||||
$scope.recalculateIframe = function () {
|
||||
var scale_width = window.innerWidth / $scope.projectorWidth;
|
||||
var scale_height = window.innerHeight / $scope.projectorHeight;
|
||||
|
||||
if (scale_width > 1 && scale_height > 1) {
|
||||
// Iframe fits in full size in the window
|
||||
$scope.scale = 1;
|
||||
$scope.iframeWidth = $scope.projectorWidth;
|
||||
$scope.iframeHeight = $scope.projectorHeight;
|
||||
} else {
|
||||
// Iframe has to be scaled down
|
||||
if (scale_width <= scale_height) {
|
||||
// width is the reference
|
||||
$scope.iframeWidth = window.innerWidth;
|
||||
$scope.scale = scale_width;
|
||||
$scope.iframeHeight = $scope.projectorHeight * scale_width;
|
||||
} else {
|
||||
// height is the reference
|
||||
$scope.iframeHeight = window.innerHeight;
|
||||
$scope.scale = scale_height;
|
||||
$scope.iframeWidth = $scope.projectorWidth * scale_height;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// watch for changes in the windowsize
|
||||
$(window).on("resize.doResize", function () {
|
||||
$scope.$apply(function() {
|
||||
$scope.recalculateIframe();
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$on("$destroy",function (){
|
||||
$(window).off("resize.doResize");
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('ProjectorCtrl', [
|
||||
'$scope',
|
||||
'Projector',
|
||||
|
@ -602,6 +602,15 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
})
|
||||
.state('projector', {
|
||||
url: '/projector',
|
||||
templateUrl: 'static/templates/projector-container.html',
|
||||
data: {extern: true},
|
||||
onEnter: function($window) {
|
||||
$window.location.href = this.url;
|
||||
}
|
||||
})
|
||||
.state('real-projector', {
|
||||
url: '/real-projector',
|
||||
templateUrl: 'static/templates/projector.html',
|
||||
data: {extern: true},
|
||||
onEnter: function($window) {
|
||||
$window.location.href = this.url;
|
||||
@ -810,6 +819,7 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
'Config',
|
||||
'gettextCatalog',
|
||||
function($parse, Config, gettextCatalog) {
|
||||
// remove resolution when changing to multiprojector
|
||||
function getHtmlType(type) {
|
||||
return {
|
||||
string: 'text',
|
||||
@ -818,6 +828,7 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
boolean: 'checkbox',
|
||||
choice: 'choice',
|
||||
colorpicker: 'colorpicker',
|
||||
resolution: 'resolution',
|
||||
}[type];
|
||||
}
|
||||
|
||||
@ -1144,6 +1155,22 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
|
||||
});
|
||||
|
||||
// watch for changes in Config
|
||||
var last_conf;
|
||||
$scope.$watch(function () {
|
||||
return Config.lastModified();
|
||||
}, function () {
|
||||
var conf = Config.get('projector_resolution').value;
|
||||
// With multiprojector, get the resolution from Prjector.get(pk).{width; height}
|
||||
if(!last_conf || last_conf.width != conf.width || last-conf.height != conf.height) {
|
||||
last_conf = conf;
|
||||
$scope.projectorWidth = conf.width;
|
||||
$scope.projectorHeight = conf.height;
|
||||
$scope.scale = 256.0 / $scope.projectorWidth;
|
||||
$scope.iframeHeight = $scope.scale * $scope.projectorHeight;
|
||||
}
|
||||
});
|
||||
|
||||
// *** countdown functions ***
|
||||
$scope.calculateCountdownTime = function (countdown) {
|
||||
countdown.seconds = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
||||
|
@ -11,6 +11,26 @@
|
||||
id="{{ key }}"
|
||||
type="{{ type }}">
|
||||
|
||||
<!-- resolution -->
|
||||
<!-- Can be removed with multiprojector, but maybe it could be reused in the projectormanage-view -->
|
||||
<!-- if removed, remember to delete the class resolution -->
|
||||
<span ng-if="type == 'resolution'">
|
||||
<translate>Width</translate>:
|
||||
<input ng-model="$parent.value.width"
|
||||
ng-model-option="{debounce: 1000}"
|
||||
ng-change="save(configOption.key, $parent.value)"
|
||||
class="form-control resolution"
|
||||
id="{{ key }}_width"
|
||||
type="number">
|
||||
<translate>Height</translate>:
|
||||
<input ng-model="$parent.value.height"
|
||||
ng-model-option="{debounce: 1000}"
|
||||
ng-change="save(configOption.key, $parent.value)"
|
||||
class="form-control resolution"
|
||||
id="{{ key }}_height"
|
||||
type="number">
|
||||
</span>
|
||||
|
||||
<!-- colorpicker -->
|
||||
<input ng-if="type == 'colorpicker'"
|
||||
colorpicker
|
||||
|
@ -9,9 +9,30 @@
|
||||
<h4 translate>Live view</h4>
|
||||
</a>
|
||||
<div uib-collapse="isLiveViewClosed" ng-cloak>
|
||||
<style>
|
||||
/* iframe for live view */
|
||||
.col2 #iframe {
|
||||
width: {{ projectorWidth }}px;
|
||||
height: {{ projectorHeight }}px;
|
||||
-moz-transform: scale({{ scale }});
|
||||
-webkit-transform: scale({{ scale }});
|
||||
-o-transform: scale({{ scale }});
|
||||
transform: scale({{ scale }});
|
||||
/* IE8+ - must be on one line, unfortunately */
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ scale }}, M12=0, M21=0, M22={{ scale }}, SizingMethod='auto expand')";
|
||||
}
|
||||
|
||||
.col2 #iframewrapper {
|
||||
height: {{ iframeHeight }}px;
|
||||
}
|
||||
|
||||
.col2 #iframeoverlay {
|
||||
height: {{ iframeHeight }}px;
|
||||
}
|
||||
</style>
|
||||
<a ui-sref="projector" target="_blank">
|
||||
<div id="iframewrapper">
|
||||
<iframe id="iframe" src="/projector" frameborder="0"></iframe>
|
||||
<iframe id="iframe" src="/real-projector" frameborder="0"></iframe>
|
||||
<div id="iframeoverlay"></div>
|
||||
</div>
|
||||
</a>
|
||||
|
45
openslides/core/static/templates/projector-container.html
Normal file
45
openslides/core/static/templates/projector-container.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<base href="/">
|
||||
<title>OpenSlides – Projector</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;">
|
||||
<link rel="stylesheet" href="static/css/openslides-libs.css">
|
||||
<link rel="stylesheet" href="static/css/projector.css">
|
||||
<link rel="icon" href="/static/img/favicon.png">
|
||||
<script src="static/js/openslides-libs.js"></script>
|
||||
|
||||
<div ng-controller="ProjectorContainerCtrl" class="pContainer">
|
||||
<div>
|
||||
<style>
|
||||
.pContainer #iframe {
|
||||
width: {{ projectorWidth }}px;
|
||||
height: {{ projectorHeight }}px;
|
||||
-moz-transform: scale({{ scale }});
|
||||
-webkit-transform: scale({{ scale }});
|
||||
-o-transform: scale({{ scale }});
|
||||
transform: scale({{ scale }});
|
||||
/* IE8+ - must be on one line, unfortunately */
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ scale }}, M12=0, M21=0, M22={{ scale }}, SizingMethod='auto expand')";
|
||||
}
|
||||
|
||||
.pContainer #iframewrapper {
|
||||
width: {{ iframeWidth }}px;
|
||||
height: {{ iframeHeight }}px;
|
||||
}
|
||||
|
||||
.pContainer #iframeoverlay {
|
||||
width: {{ iframeWidth }}px;
|
||||
height: {{ iframeHeight }}px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="iframewrapper">
|
||||
<iframe id="iframe" src="/real-projector" frameborder="0"></iframe>
|
||||
<div id="iframeoverlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/angular_js/projector/"></script>
|
@ -1,6 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<base href="/">
|
||||
<title>OpenSlides – Projector</title>
|
||||
<link rel="stylesheet" href="static/css/openslides-libs.css">
|
||||
|
@ -27,9 +27,12 @@ urlpatterns = [
|
||||
views.AppsJsView.as_view(),
|
||||
name='core_apps_js'),
|
||||
|
||||
# View for the projectors are handelt by angular.
|
||||
# View for the projectors are handled by angular.
|
||||
url(r'^projector.*$', views.ProjectorView.as_view()),
|
||||
|
||||
# Original view without resolutioncontrol for the projectors are handled by angular.
|
||||
url(r'^real-projector.*$', views.RealProjectorView.as_view()),
|
||||
|
||||
# Main entry point for all angular pages.
|
||||
# Has to be the last entry in the urls.py
|
||||
url(r'^.*$', views.IndexView.as_view()),
|
||||
|
@ -66,6 +66,20 @@ class ProjectorView(utils_views.View):
|
||||
"""
|
||||
The primary view for OpenSlides projector using AngularJS.
|
||||
|
||||
The projector container template is 'openslides/core/static/templates/projector-container.html'.
|
||||
This container is for controlling the projector resolution.
|
||||
"""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
with open(finders.find('templates/projector-container.html')) as f:
|
||||
content = f.read()
|
||||
return HttpResponse(content)
|
||||
|
||||
|
||||
class RealProjectorView(utils_views.View):
|
||||
"""
|
||||
The original view without resolutioncontrol for OpenSlides projector using AngularJS.
|
||||
|
||||
The default base template is 'openslides/core/static/templates/projector.html'.
|
||||
You can override it by simply adding a custom 'templates/projector.html'
|
||||
file to the custom staticfiles directory. See STATICFILES_DIRS in
|
||||
@ -168,7 +182,7 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
|
||||
elif self.action in ('metadata', 'list'):
|
||||
result = self.request.user.has_perm('core.can_see_projector')
|
||||
elif self.action in ('activate_elements', 'prune_elements', 'update_elements',
|
||||
'deactivate_elements', 'clear_elements', 'control_view'):
|
||||
'deactivate_elements', 'clear_elements', 'control_view', 'set_resolution'):
|
||||
result = (self.request.user.has_perm('core.can_see_projector') and
|
||||
self.request.user.has_perm('core.can_manage_projector'))
|
||||
else:
|
||||
@ -314,6 +328,46 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def set_resolution(self, request, pk):
|
||||
"""
|
||||
REST API operation to set the resolution.
|
||||
|
||||
It is actually unused, because the resolution is currently set in the config.
|
||||
But with the multiprojector feature this will become importent to set the
|
||||
resolution per projector individually.
|
||||
|
||||
It expects a POST request to
|
||||
/rest/core/projector/<pk>/set_resolution/ with a dictionary with the width
|
||||
and height and the values.
|
||||
|
||||
Example:
|
||||
|
||||
{
|
||||
"width": "1024",
|
||||
"height": "768"
|
||||
}
|
||||
"""
|
||||
if not isinstance(request.data, dict):
|
||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
||||
if request.data.get('width') is None or request.data.get('height') is None:
|
||||
raise ValidationError({'detail': 'A width and a height have to be given.'})
|
||||
if not isinstance(request.data['width'], int) or not isinstance(request.data['height'], int):
|
||||
raise ValidationError({'detail': 'Data has to be integers.'})
|
||||
if (request.data['width'] < 800 or request.data['width'] > 3840 or
|
||||
request.data['height'] < 600 or request.data['height'] > 2160):
|
||||
raise ValidationError({'detail': 'The Resolution have to be between 800x600 and 3840x2160.'})
|
||||
|
||||
projector_instance = self.get_object()
|
||||
projector_instance.width = request.data['width']
|
||||
projector_instance.height = request.data['height']
|
||||
projector_instance.save()
|
||||
|
||||
message = 'Changing resolution to {width}x{height} was successful.'.format(
|
||||
width=request.data['width'],
|
||||
height=request.data['height'])
|
||||
return Response({'detail': message})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def control_view(self, request, pk):
|
||||
"""
|
||||
|
@ -34,7 +34,9 @@ class ProjectorAPI(TestCase):
|
||||
'uuid': 'aae4a07b26534cfb9af4232f361dce73',
|
||||
'name': 'core/customslide'}},
|
||||
'scale': 0,
|
||||
'scroll': 0})
|
||||
'scroll': 0,
|
||||
'width': 1024,
|
||||
'height': 768})
|
||||
|
||||
def test_invalid_slide_on_default_projector(self):
|
||||
self.client.login(username='admin', password='admin')
|
||||
@ -54,7 +56,9 @@ class ProjectorAPI(TestCase):
|
||||
'uuid': 'fc6ef43b624043068c8e6e7a86c5a1b0',
|
||||
'error': 'Projector element does not exist.'}},
|
||||
'scale': 0,
|
||||
'scroll': 0})
|
||||
'scroll': 0,
|
||||
'width': 1024,
|
||||
'height': 768})
|
||||
|
||||
|
||||
class VersionView(TestCase):
|
||||
|
Loading…
Reference in New Issue
Block a user