diff --git a/openslides/core/config.py b/openslides/core/config.py index 939a92fd3..d1989da8e 100644 --- a/openslides/core/config.py +++ b/openslides/core/config.py @@ -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}) diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py index 3e5a3e170..60c952d3c 100644 --- a/openslides/core/config_variables.py +++ b/openslides/core/config_variables.py @@ -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') diff --git a/openslides/core/migrations/0004_projector_resolution.py b/openslides/core/migrations/0004_projector_resolution.py new file mode 100644 index 000000000..7d2417784 --- /dev/null +++ b/openslides/core/migrations/0004_projector_resolution.py @@ -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), + ), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index 107cb4506..f93dcbfd6 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -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. diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index 39dfd971a..5adac0e20 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -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): diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index 266781896..e49f37135 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -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; diff --git a/openslides/core/static/css/projector.css b/openslides/core/static/css/projector.css index 6bb6d300d..3267a36da 100644 --- a/openslides/core/static/css/projector.css +++ b/openslides/core/static/css/projector.css @@ -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); diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index 8747da906..6b46cfaa7 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -286,7 +286,6 @@ angular.module('OpenSlidesApp.core', [ } ]) - .factory('jsDataModel', [ '$http', 'Projector', diff --git a/openslides/core/static/js/core/projector.js b/openslides/core/static/js/core/projector.js index dc907fc7b..4fc269649 100644 --- a/openslides/core/static/js/core/projector.js +++ b/openslides/core/static/js/core/projector.js @@ -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', diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index a178bee5f..a264914fd 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -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 ); diff --git a/openslides/core/static/templates/config-form-field.html b/openslides/core/static/templates/config-form-field.html index ab6f2c8c5..63f237629 100644 --- a/openslides/core/static/templates/config-form-field.html +++ b/openslides/core/static/templates/config-form-field.html @@ -11,6 +11,26 @@ id="{{ key }}" type="{{ type }}"> + + + + + Width: + + Height: + + + Live view
+
- +
diff --git a/openslides/core/static/templates/projector-container.html b/openslides/core/static/templates/projector-container.html new file mode 100644 index 000000000..24a958f72 --- /dev/null +++ b/openslides/core/static/templates/projector-container.html @@ -0,0 +1,45 @@ + + + + + +OpenSlides – Projector + + + + + + +
+
+ + +
+ +
+
+
+
+ + diff --git a/openslides/core/static/templates/projector.html b/openslides/core/static/templates/projector.html index 571248f35..c58bf49e0 100644 --- a/openslides/core/static/templates/projector.html +++ b/openslides/core/static/templates/projector.html @@ -1,6 +1,7 @@ + OpenSlides – Projector diff --git a/openslides/core/urls.py b/openslides/core/urls.py index 3dbc1b614..e7478824c 100644 --- a/openslides/core/urls.py +++ b/openslides/core/urls.py @@ -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()), diff --git a/openslides/core/views.py b/openslides/core/views.py index 7c38e257d..7669a5a02 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -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//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): """ diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py index 49640153e..8c33e2b71 100644 --- a/tests/integration/core/test_views.py +++ b/tests/integration/core/test_views.py @@ -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):