Scroll projector to a given line
This commit is contained in:
parent
011ec56b88
commit
252ba02e86
@ -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)
|
||||
========================
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 = [
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user