Scroll projector to a given line

This commit is contained in:
FinnStutzenstein 2018-04-18 13:24:30 +02:00 committed by Emanuel Schütze
parent 011ec56b88
commit 252ba02e86
7 changed files with 164 additions and 53 deletions

View File

@ -7,6 +7,9 @@ https://openslides.org/
Version 2.3 (unreleased)
========================
Motions:
- New feature to schroll the projector to a specific line [#3748].
Version 2.2 (2018-06-06)
========================

View File

@ -485,7 +485,7 @@ angular.module('OpenSlidesApp.core', [
deregisterCallbacks: function () {
_.forEach(arguments, this.deregisterCallback);
},
notify: function(eventName, params, users, channels) {
notify: function(eventName, params, users, channels, projectors) {
if (eventNameRegex.test(eventName)) {
if (!params || typeof params !== 'object') {
params = {};
@ -497,6 +497,7 @@ angular.module('OpenSlidesApp.core', [
params: params,
users: users,
replyChannels: channels,
projectors: projectors,
};
if (!operator.user) {
if (!anonymousTrackId) {

View File

@ -21,13 +21,40 @@ angular.module('OpenSlidesApp.motions.projector', [
'Motion',
'MotionChangeRecommendation',
'User',
function($scope, Motion, MotionChangeRecommendation, User) {
'Notify',
'ProjectorID',
function($scope, Motion, MotionChangeRecommendation, User, Notify, ProjectorID) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
$scope.mode = $scope.element.mode || 'original';
var notifyNamePrefix = 'projector_' + ProjectorID() + '_motion_line_';
var callbackId = Notify.registerCallback(notifyNamePrefix + 'request', function (params) {
var line = params.params.line;
if (!line) {
return;
}
var scrollTop = null;
$('.line-number-' + line).each(function() {
var top = $(this).offset().top;
if (scrollTop === null || top < scrollTop) {
scrollTop = top;
}
});
if (scrollTop) {
scrollTop += (-$scope.scroll); // Add the (reversed) scrolling ontop
var scroll = Math.floor((scrollTop/250) - 0.2);
var channel = params.senderReplyChannelName;
Notify.notify(notifyNamePrefix + 'answer', {scroll: scroll}, null, [channel], null);
}
});
$scope.$on('$destroy', function () {
Notify.deregisterCallback(callbackId);
});
Motion.bindOne(id, $scope, 'motion');
User.bindAll({}, $scope, 'users');

View File

@ -1415,6 +1415,7 @@ angular.module('OpenSlidesApp.motions.site', [
'MotionBlock',
'MotionPdfExport',
'PersonalNoteManager',
'Notify',
'WebpageTitle',
'EditingWarning',
function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog,
@ -1422,7 +1423,7 @@ angular.module('OpenSlidesApp.motions.site', [
MotionStateAndRecommendationParser, MotionChangeRecommendation, Motion, MotionComment,
Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing,
MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock,
MotionPdfExport, PersonalNoteManager, WebpageTitle, EditingWarning) {
MotionPdfExport, PersonalNoteManager, Notify, WebpageTitle, EditingWarning) {
var motion = Motion.get(motionId);
Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles');
@ -1538,13 +1539,16 @@ angular.module('OpenSlidesApp.motions.site', [
Motion.bindOne(motion.parent_id, $scope, 'parent');
}
$scope.scrollToLine = 0;
$scope.highlight = 0;
$scope.linesForProjector = false;
$scope.scrollToAndHighlight = function (line) {
$scope.scrollToLine = line;
$scope.highlight = line;
// The same line number can occur twice in diff view; we scroll to the first one in this case
var scrollTop = null;
$(".line-number-" + line).each(function() {
$('.line-number-' + line).each(function() {
var top = $(this).offset().top;
if (top > 0 && (scrollTop === null || top < scrollTop)) {
scrollTop = top;
@ -1561,6 +1565,29 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.highlight = 0;
}, 2000);
}
$scope.scrollProjectorToLine(line);
};
$scope.scrollProjectorToLine = function (line) {
var projectorIds = $scope.motion.isProjected();
if (!$scope.linesForProjector || !line || !projectorIds.length) {
return;
}
var projectorId = projectorIds[0];
var notifyNamePrefix = 'projector_' + projectorId + '_motion_line_';
// register callback
var callbackId = Notify.registerCallback(notifyNamePrefix + 'answer', function (params) {
Notify.deregisterCallback(callbackId);
$http.post('/rest/core/projector/' + projectorId + '/set_scroll/', params.params.scroll);
});
// Query all projectors
Notify.notify(notifyNamePrefix + 'request', {line: line}, null, null, [projectorId]);
};
$scope.toggleLinesForProjector = function () {
$scope.linesForProjector = !$scope.linesForProjector;
$scope.scrollProjectorToLine($scope.scrollToLine);
};
// open edit dialog

View File

@ -79,14 +79,20 @@
e-formclass="small-form"
onaftersave="scrollToAndHighlight(gotoLinenumber)">
</span>
<button type="button" class="btn btn-sm btn-default"
ng-click="lineNumberForm.$show()"
ng-if="lineNumberMode != 'none'"
uib-tooltip="{{ 'Jump to a given line number' | translate }}"
tooltip-placement="bottom">
<i class="fa fa-share"></i>
<translate>go</translate>
</button>
<div class="btn-group" ng-if="lineNumberMode != 'none'">
<button type="button" class="btn btn-sm btn-default" ng-click="lineNumberForm.$show()">
<i class="fa fa-share"></i>
<translate>go</translate>
</button>
<button type="button" class="btn btn-sm btn-default"
ng-if="lineNumberMode != 'none' && motion.isProjected().length &&
operator.hasPerms('core.can_manage_projector')"
ng-click="toggleLinesForProjector()"
uib-tooltip="{{ 'Show highlighted line also on projector.' | translate }}">
<i class="fa" ng-class="linesForProjector ? 'fa-check-square-o' : 'fa-square-o'"></i>&nbsp;
<i class="fa fa-video-camera"></i>
</button>
</div>
</div>
</div>
</div>

View File

@ -7,12 +7,14 @@ from openslides.utils.autoupdate import (
ws_add_site,
ws_disconnect_projector,
ws_disconnect_site,
ws_receive_projector,
ws_receive_site,
)
projector_routing = [
route("websocket.connect", ws_add_projector),
route("websocket.disconnect", ws_disconnect_projector),
route("websocket.receive", ws_receive_projector),
]
site_routing = [

View File

@ -123,9 +123,8 @@ def ws_disconnect_site(message: Any) -> None:
@channel_session_user
def ws_receive_site(message: Any) -> None:
"""
This function is called if a message from a client comes in. The message
should be a list. Every item is broadcasted to the given users (or all
users if no user list is given) if it is a notify element.
If we recieve something from the client we currently just interpret this
as a notify message.
The server adds the sender's user id (0 for anonymous) and reply
channel name so that a receiver client may reply to the sender or to all
@ -138,51 +137,76 @@ def ws_receive_site(message: Any) -> None:
pass
else:
if isinstance(incomming, list):
# Parse all items
receivers_users = defaultdict(list) # type: Dict[int, List[Any]]
receivers_reply_channels = defaultdict(list) # type: Dict[str, List[Any]]
items_for_all = []
for item in incomming:
if item.get('collection') == 'notify':
use_receivers_dict = False
item['senderReplyChannelName'] = message.reply_channel.name
item['senderUserId'] = message.user.id or 0
notify(
incomming,
senderReplyChannelName=message.reply_channel.name,
senderUserId=message.user.id or 0)
# Force the params to be a dict
if not isinstance(item.get('params'), dict):
item['params'] = {}
users = item.get('users')
if isinstance(users, list):
# Send this item only to all reply channels of some site users.
for user_id in users:
receivers_users[user_id].append(item)
use_receivers_dict = True
def notify(incomming: List[Dict[str, Any]], **attributes: Any) -> None:
"""
The incomming should be a list of notify elements. Every item is broadcasted
to the given users, channels or projectors. If none is given, the message is
send to each site client.
"""
# Parse all items
receivers_users = defaultdict(list) # type: Dict[int, List[Any]]
receivers_projectors = defaultdict(list) # type: Dict[int, List[Any]]
receivers_reply_channels = defaultdict(list) # type: Dict[str, List[Any]]
items_for_all = []
for item in incomming:
if item.get('collection') == 'notify':
use_receivers_dict = False
reply_channels = item.get('replyChannels')
if isinstance(reply_channels, list):
# Send this item only to some reply channels.
for reply_channel_name in reply_channels:
receivers_reply_channels[reply_channel_name].append(item)
use_receivers_dict = True
for key, value in attributes.items():
item[key] = value
if not use_receivers_dict:
# Send this item to all reply channels.
items_for_all.append(item)
# Force the params to be a dict
if not isinstance(item.get('params'), dict):
item['params'] = {}
# Send all items
for user_id, channel_names in websocket_user_cache.get_all().items():
output = receivers_users[user_id]
if len(output) > 0:
for channel_name in channel_names:
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
users = item.get('users')
if isinstance(users, list):
# Send this item only to all reply channels of some site users.
for user_id in users:
receivers_users[user_id].append(item)
use_receivers_dict = True
for channel_name, output in receivers_reply_channels.items():
if len(output) > 0:
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
projectors = item.get('projectors')
if isinstance(projectors, list):
# Send this item only to all reply channels of some site users.
for projector_id in projectors:
receivers_projectors[projector_id].append(item)
use_receivers_dict = True
if len(items_for_all) > 0:
send_or_wait(Group('site').send, {'text': json.dumps(items_for_all)})
reply_channels = item.get('replyChannels')
if isinstance(reply_channels, list):
# Send this item only to some reply channels.
for reply_channel_name in reply_channels:
receivers_reply_channels[reply_channel_name].append(item)
use_receivers_dict = True
if not use_receivers_dict:
# Send this item to all reply channels.
items_for_all.append(item)
# Send all items
for user_id, channel_names in websocket_user_cache.get_all().items():
output = receivers_users[user_id]
if len(output) > 0:
for channel_name in channel_names:
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
for channel_name, output in receivers_reply_channels.items():
if len(output) > 0:
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
for projector_id, output in receivers_projectors.items():
if len(output) > 0:
send_or_wait(Group('projector-{}'.format(projector_id)).send, {'text': json.dumps(output)})
if len(items_for_all) > 0:
send_or_wait(Group('site').send, {'text': json.dumps(items_for_all)})
@channel_session_user_from_http
@ -247,6 +271,27 @@ def ws_disconnect_projector(message: Any, projector_id: int) -> None:
Group('projector-all').discard(message.reply_channel)
def ws_receive_projector(message: Any, projector_id: int) -> None:
"""
If we recieve something from the client we currently just interpret this
as a notify message.
The server adds the sender's projector id and reply channel name so that
a receiver client may reply to the sender or to all sender's instances.
"""
try:
incomming = json.loads(message.content['text'])
except ValueError:
# Message content is invalid. Just do nothing.
pass
else:
if isinstance(incomming, list):
notify(
incomming,
senderReplyChannelName=message.reply_channel.name,
senderProjectorId=projector_id)
def send_data_projector(message: ChannelMessageFormat) -> None:
"""
Informs all projector clients about changed data.