diff --git a/ui/cache.js b/ui/cache.js
new file mode 100755
index 00000000..e51d4600
--- /dev/null
+++ b/ui/cache.js
@@ -0,0 +1,85 @@
+/**********************************************************************
+*
+*
+*
+**********************************************************************/
+
+//var DEBUG = DEBUG != null ? DEBUG : true
+//
+var IMAGE_CACHE = []
+
+
+
+/*********************************************************************/
+
+// TODO add global cache...
+// - manage cache by number and preview size...
+// - keep in biggish...
+
+
+// NOTE: this will always overwrite the previous cache set for a ribbon...
+// NOTE: it appears that sorting images by priority before loading them
+// to cache has little or no effect on the order they are
+// loaded/rendered...
+// NOTE: this is not meant to be a real cache, rather a que for the OS and
+// backend/webkit on what's next...
+//
+// XXX this appears to actually make things slow and laggy...
+function preCacheRibbonImages(ribbon){
+ var deferred = $.Deferred()
+ setTimeout(function(){
+ var i = getRibbonIndex(ribbon)
+ var size = getVisibleImageSize('max')
+ var screen_size = getScreenWidthInImages(getVisibleImageSize())
+ // XXX needs tuning...
+ var cache_frame_size = (screen_size * LOAD_SCREENS)
+ var images = ribbon.find('.image')
+ var first = getImageGID(images.first())
+ var last = getImageGID(images.last())
+
+ var gids = getGIDsAfter(-cache_frame_size, first, i)
+ .concat(getGIDsAfter(cache_frame_size, last, i))
+
+ var cache = []
+ IMAGE_CACHE[i] = cache
+ $.each(gids, function(i, e){
+ var img = new Image()
+ img.src = getBestPreview(e, size).url
+ cache.push(img)
+ })
+
+ deferred.resolve(cache)
+ }, 0)
+ return deferred
+}
+
+
+function preCacheAllRibbons(){
+ $('.ribbon').each(function(){
+ preCacheRibbonImages($(this))
+ })
+ return IMAGE_CACHE
+}
+
+
+
+/*********************************************************************/
+
+function setupImageCache(viewer){
+ console.log('Image cache: setup...')
+
+ return viewer
+ .on('reloadedRibbon updatedRibbon', function(evt, ribbon){
+
+ window.DEBUG
+ && console.log('>>> (ribbon:', getRibbonIndex(ribbon), ') Updating cache...')
+
+ preCacheRibbonImages(ribbon)
+ })
+}
+SETUP_BINDINGS.push(setupImageCache)
+
+
+
+/**********************************************************************
+* vim:set ts=4 sw=4 : */
diff --git a/ui/data.js b/ui/data.js
index fda015bf..bfcd2f85 100755
--- a/ui/data.js
+++ b/ui/data.js
@@ -138,8 +138,6 @@ var SETTINGS = {
var BASE_URL = '.'
-var IMAGE_CACHE = []
-
// XXX make these usable for both saving and loading...
// XXX get these from config...
var IMAGES_FILE_DEFAULT = 'images.json'
@@ -160,13 +158,6 @@ var UPDATE_SYNC = false
var SYNC_IMG_LOADER = false
-// list of functions to setup different bindings
-//
-// each function must be of the form:
-// setupBinding(viewer) -> viewer
-//
-var SETUP_BINDINGS = []
-
// list of function that update image state...
//
// each function must be of the form:
@@ -1778,61 +1769,6 @@ function loadSettings(){
-/**********************************************************************
-* Image caching...
-*/
-
-// TODO add global cache...
-// - manage cache by number and preview size...
-// - keep in biggish...
-
-
-// NOTE: this will always overwrite the previous cache set for a ribbon...
-// NOTE: it appears that sorting images by priority before loading them
-// to cache has little or no effect on the order they are
-// loaded/rendered...
-// NOTE: this is not meant to be a real cache, rather a que for the OS and
-// backend/webkit on what's next...
-//
-// XXX this appears to actually make things slower and laggy...
-function preCacheRibbonImages(ribbon){
- var deferred = $.Deferred()
- setTimeout(function(){
- var i = getRibbonIndex(ribbon)
- var size = getVisibleImageSize('max')
- var screen_size = getScreenWidthInImages(getVisibleImageSize())
- // XXX needs tuning...
- var cache_frame_size = (screen_size * LOAD_SCREENS)
- var images = ribbon.find('.image')
- var first = getImageGID(images.first())
- var last = getImageGID(images.last())
-
- var gids = getGIDsAfter(-cache_frame_size, first, i)
- .concat(getGIDsAfter(cache_frame_size, last, i))
-
- var cache = []
- IMAGE_CACHE[i] = cache
- $.each(gids, function(i, e){
- var img = new Image()
- img.src = getBestPreview(e, size).url
- cache.push(img)
- })
-
- deferred.resolve(cache)
- }, 0)
- return deferred
-}
-
-
-function preCacheAllRibbons(){
- $('.ribbon').each(function(){
- preCacheRibbonImages($(this))
- })
- return IMAGE_CACHE
-}
-
-
-
/**********************************************************************
* Actions...
*/
@@ -1964,5 +1900,252 @@ function loadRibbonsFromPath(path, cmp, reverse, dir_name){
+/*********************************************************************/
+
+function setupData(viewer){
+ console.log('Data: setup...')
+
+ return viewer
+ // NOTE: we do not need to worry about explicit centering the ribbon
+ // here, just ball-park-load the correct batch...
+ // NOTE: if we decide to hide ribbons, uncomment the visibility
+ // test down in the code...
+ .on('preCenteringRibbon', function(evt, ribbon, image){
+ var r = getRibbonIndex(ribbon)
+
+ // skip all but the curent ribbon in single image view...
+ if(toggleSingleImageMode('?') == 'on' && r != getRibbonIndex()){
+ return
+ }
+
+ // prepare for loading...
+ var gid = getImageGID(image)
+ var gr = DATA.ribbons[r]
+
+ // NOTE: this can return null in certain cases (see docs)
+ var gid_before = getGIDBefore(gid, r)
+ // we'll set the image to the first if the align target is
+ // before it (i.e. gid_before is null)...
+ var img_before = gid_before == null
+ ? ribbon.find('.image').first()
+ : getImageBefore(image, ribbon)
+ gid_before = gid_before == null ? gr[0] : gid_before
+
+ var screen_size = getScreenWidthInImages()
+ screen_size = screen_size < 1 ? 1 : screen_size
+ var load_frame_size = Math.round(screen_size * LOAD_SCREENS)
+
+ // target image is loaded...
+ if(gid_before == getImageGID(img_before)){
+ var roll_frame_size = Math.ceil(load_frame_size * ROLL_FRAME)
+ var threshold = Math.floor(load_frame_size * LOAD_THRESHOLD)
+ threshold = threshold < 1 ? 1 : threshold
+
+ var head = img_before.prevAll('.image').length
+ var tail = img_before.nextAll('.image').length
+ var l = ribbon.find('.image').length
+ var index = gr.indexOf(gid_before)
+ var at_start = index < threshold
+ var at_end = (gr.length-1 - index) < threshold
+
+ // less images than expected - extend ribbon...
+ if(l < load_frame_size){
+ // NOTE: we are forcing the count of images...
+ loadImagesAround(load_frame_size, gid, ribbon, null, true)
+
+ // tail at threshold - roll ->
+ } else if(!at_end && tail < threshold){
+ var rolled = rollImages(roll_frame_size, ribbon)
+
+ // head at threshold - roll <-
+ } else if(!at_start && head < threshold){
+ var rolled = rollImages(-roll_frame_size, ribbon)
+
+ //} else {
+ // console.log('>>> skipping:', r)
+ }
+
+ // we jumped, load new set...
+ } else {
+ // NOTE: we are forcing the count of images...
+ loadImagesAround(load_frame_size, gid, ribbon, null, true)
+ }
+ })
+
+
+ .on('shiftedImage', function(evt, image, from, to){
+ from = getRibbonIndex(from)
+ //var ribbon = to
+ to = getRibbonIndex(to)
+
+ var gid = getImageGID(image)
+ var after = getGIDBefore(gid, to)
+
+ // remove the elem from the from ribbon...
+ var index = DATA.ribbons[from].indexOf(gid)
+ var img = DATA.ribbons[from].splice(index, 1)
+
+ // put the elem in the to ribbon...
+ index = after == null ? 0 : DATA.ribbons[to].indexOf(after) + 1
+ DATA.ribbons[to].splice(index, 0, gid)
+
+ // indicators...
+ flashIndicator(from < to ? 'next' : 'prev')
+ })
+
+
+ .on('createdRibbon', function(evt, index){
+ index = getRibbonIndex(index)
+ DATA.ribbons.splice(index, 0, [])
+ })
+ .on('removedRibbon', function(evt, index){
+ DATA.ribbons.splice(index, 1)
+ })
+
+
+ .on('requestedFirstImage', function(evt, ribbon){
+ var r = getRibbonIndex(ribbon)
+ var gr = DATA.ribbons[r]
+ rollImages(-gr.length, ribbon)
+ })
+ .on('requestedLastImage', function(evt, ribbon){
+ var r = getRibbonIndex(ribbon)
+ var gr = DATA.ribbons[r]
+ rollImages(gr.length, ribbon)
+ })
+
+ .on('fittingImages', function(evt, n){
+ //console.log('!!!! fittingImages')
+ // load correct amount of images in each ribbon!!!
+ var screen_size = getScreenWidthInImages()
+ var gid = getImageGID()
+
+ /* XXX used to skip ribbons that are not visible... (see bellow)
+ var viewer = $('.viewer')
+ var H = viewer.height()
+ var h = getImage().height()
+ */
+
+ // update and align ribbons...
+ $('.ribbon').each(function(){
+ var r = $(this)
+ /* XXX skip ribbons that are not visible...
+ * causes misaligns and misloads on zoom-in...
+ // NOTE: we factor in the scale difference to predict
+ // ribbon position in the new view...
+ var t = getRelativeVisualPosition(viewer, r).top * (n/screen_size)
+ if( t+h <= 0 || t >= H ){
+ console.log('#### skipping align of ribbon:', getRibbonIndex(r))
+ return
+ }
+ */
+ loadImagesAround(Math.round(screen_size * LOAD_SCREENS), gid, r, null, true)
+ })
+
+ centerView(null, 'css')
+
+ // update settings...
+ if(toggleSingleImageMode('?') == 'on'){
+ SETTINGS['single-image-mode-screen-images'] = n
+ } else {
+ SETTINGS['ribbon-mode-screen-images'] = n
+ }
+
+ // update proportions...
+ if(window.PROPORTIONS_RATIO_THRESHOLD != null
+ && toggleSingleImageMode('?') == 'on'){
+
+ var h = getVisibleImageSize('height')
+ var w = getVisibleImageSize('width')
+ var H = $('.viewer').innerHeight()
+ var W = $('.viewer').innerWidth()
+
+ var m = Math.min(W/w, H/h)
+
+ if(m < PROPORTIONS_RATIO_THRESHOLD){
+ toggleImageProportions('fit-viewer')
+ } else {
+ toggleImageProportions('none')
+ }
+ }
+
+ // update size classes...
+ // XXX make thresholds global...
+ if(n <= 2.5){
+ $('.viewer')
+ .removeClass('small')
+ .addClass('large')
+ } else if (n >= 6) {
+ $('.viewer')
+ .addClass('small')
+ .removeClass('large')
+ } else {
+ $('.viewer')
+ .removeClass('small')
+ .removeClass('large')
+ }
+
+ // update previews...
+ updateImages()
+ })
+
+
+ .on('focusingImage', function(evt, image){
+ image = $(image)
+ DATA.current = getImageGID(image)
+
+ if(window.setWindowTitle != null){
+ // XXX do we need to hide the extension...
+ setWindowTitle(getImageFileName())
+ //.split(/\.(jpg|jpeg|png|gif)$/)[0])
+ }
+ })
+
+
+ // basic image manipulation...
+ .on('rotatingLeft rotatingRight', function(evt, image){
+ $(image).each(function(i, e){
+ var img = $(this)
+ var gid = getImageGID(img)
+ var orientation = img.attr('orientation')
+
+ // change the image orientation status and add to
+ // updated list...
+ IMAGES[gid].orientation = orientation
+ imageUpdated(gid)
+ })
+ })
+ .on('flippingVertical flippingHorizontal', function(evt, image){
+ $(image).each(function(i, e){
+ var img = $(this)
+ var gid = getImageGID(img)
+ var flip = getImageFlipState(img)
+
+ IMAGES[gid].flipped = flip
+ imageUpdated(gid)
+ })
+ })
+ .on('resetToOriginalImage', function(evt, image){
+ $(image).each(function(i, e){
+ var img = $(this)
+ var gid = getImageGID(img)
+
+ IMAGES[gid].flipped = null
+ IMAGES[gid].orientation = 0
+
+ imageUpdated(gid)
+ })
+ })
+
+
+ .on('baseURLChanged', function(evt, url){
+ saveLocalStorageBaseURL()
+ saveLocalStorageBaseURLHistory()
+ })
+}
+SETUP_BINDINGS.push(setupData)
+
+
+
/**********************************************************************
* vim:set ts=4 sw=4 spell : */
diff --git a/ui/index.html b/ui/index.html
index 4f458f0e..9ee42047 100755
--- a/ui/index.html
+++ b/ui/index.html
@@ -25,8 +25,12 @@
+
+
+
+
@@ -38,12 +42,10 @@
-
-
diff --git a/ui/info.js b/ui/info.js
index 2f24045a..c874db84 100755
--- a/ui/info.js
+++ b/ui/info.js
@@ -165,5 +165,53 @@ function inlineImageInfoHoverHandler(evt){
+/*********************************************************************/
+function setupInfo(viewer){
+ console.log('Info: setup...')
+
+ return viewer
+ // info...
+ .on('focusingImage',
+ function(){
+ showRibbonIndicator()
+ })
+ .on([
+ 'focusedNextRibbon',
+ 'focusedPrevRibbon'
+ ].join(' '),
+ function(){
+ if(toggleSingleImageMode('?') == 'on'){
+ flashRibbonIndicator()
+ }
+ })
+ .on([
+ 'rotatingLeft',
+ 'rotateingRight',
+ 'flippingVertical',
+ 'flippingHorizontal'
+ ].join(' '),
+ function(evt, image){
+ updateGlobalImageInfo($(image))
+ })
+ .on([
+ 'focusingImage',
+ 'togglingMark',
+ 'togglingBookmark',
+ 'removeingAllMarks',
+ 'removeingRibbonMarks',
+ 'markingAll',
+ 'markingRibbon',
+ 'invertingMarks'
+ ].join(' '),
+ function(){
+ updateGlobalImageInfo()
+ updateContextIndicators()
+ })
+
+
+}
+SETUP_BINDINGS.push(setupInfo)
+
+
/**********************************************************************
* vim:set ts=4 sw=4 spell nowrap : */
diff --git a/ui/setup.js b/ui/setup.js
index b29541e5..51afc19c 100755
--- a/ui/setup.js
+++ b/ui/setup.js
@@ -11,6 +11,14 @@ var PROPORTIONS_RATIO_THRESHOLD = 1.5
var CONTEXT_INDICATOR_UPDATERS = []
+// list of functions to setup different bindings
+//
+// each function must be of the form:
+// setupBinding(viewer) -> viewer
+//
+var SETUP_BINDINGS = []
+
+
/**********************************************************************
* Setup
@@ -69,306 +77,6 @@ function setupDataBindings(viewer){
setup(viewer)
})
- viewer
- .click(function(){
- if($('.ribbon').length == 0){
- loadDirectoryDialog()
- }
- })
-
- .on([
- 'focusingImage',
- 'fittingImages'
- ].join(' '),
- function(){
- updateCurrentMarker()
- })
-
- // NOTE: we do not need to worry about explicit centering the ribbon
- // here, just ball-park-load the correct batch...
- // NOTE: if we decide to hide ribbons, uncomment the visibility
- // test down in the code...
- .on('preCenteringRibbon', function(evt, ribbon, image){
- var r = getRibbonIndex(ribbon)
-
- // skip all but the curent ribbon in single image view...
- if(toggleSingleImageMode('?') == 'on' && r != getRibbonIndex()){
- return
- }
-
- // prepare for loading...
- var gid = getImageGID(image)
- var gr = DATA.ribbons[r]
-
- // NOTE: this can return null in certain cases (see docs)
- var gid_before = getGIDBefore(gid, r)
- // we'll set the image to the first if the align target is
- // before it (i.e. gid_before is null)...
- var img_before = gid_before == null
- ? ribbon.find('.image').first()
- : getImageBefore(image, ribbon)
- gid_before = gid_before == null ? gr[0] : gid_before
-
- var screen_size = getScreenWidthInImages()
- screen_size = screen_size < 1 ? 1 : screen_size
- var load_frame_size = Math.round(screen_size * LOAD_SCREENS)
-
- // target image is loaded...
- if(gid_before == getImageGID(img_before)){
- var roll_frame_size = Math.ceil(load_frame_size * ROLL_FRAME)
- var threshold = Math.floor(load_frame_size * LOAD_THRESHOLD)
- threshold = threshold < 1 ? 1 : threshold
-
- var head = img_before.prevAll('.image').length
- var tail = img_before.nextAll('.image').length
- var l = ribbon.find('.image').length
- var index = gr.indexOf(gid_before)
- var at_start = index < threshold
- var at_end = (gr.length-1 - index) < threshold
-
- // less images than expected - extend ribbon...
- if(l < load_frame_size){
- // NOTE: we are forcing the count of images...
- loadImagesAround(load_frame_size, gid, ribbon, null, true)
-
- // tail at threshold - roll ->
- } else if(!at_end && tail < threshold){
- var rolled = rollImages(roll_frame_size, ribbon)
-
- // head at threshold - roll <-
- } else if(!at_start && head < threshold){
- var rolled = rollImages(-roll_frame_size, ribbon)
-
- //} else {
- // console.log('>>> skipping:', r)
- }
-
- // we jumped, load new set...
- } else {
- // NOTE: we are forcing the count of images...
- loadImagesAround(load_frame_size, gid, ribbon, null, true)
- }
- })
-
-
- .on('shiftedImage', function(evt, image, from, to){
- from = getRibbonIndex(from)
- //var ribbon = to
- to = getRibbonIndex(to)
-
- var gid = getImageGID(image)
- var after = getGIDBefore(gid, to)
-
- // remove the elem from the from ribbon...
- var index = DATA.ribbons[from].indexOf(gid)
- var img = DATA.ribbons[from].splice(index, 1)
-
- // put the elem in the to ribbon...
- index = after == null ? 0 : DATA.ribbons[to].indexOf(after) + 1
- DATA.ribbons[to].splice(index, 0, gid)
-
- // indicators...
- flashIndicator(from < to ? 'next' : 'prev')
- })
-
-
- .on('createdRibbon', function(evt, index){
- index = getRibbonIndex(index)
- DATA.ribbons.splice(index, 0, [])
- })
- .on('removedRibbon', function(evt, index){
- DATA.ribbons.splice(index, 1)
- })
-
-
- .on('requestedFirstImage', function(evt, ribbon){
- var r = getRibbonIndex(ribbon)
- var gr = DATA.ribbons[r]
- rollImages(-gr.length, ribbon)
- })
- .on('requestedLastImage', function(evt, ribbon){
- var r = getRibbonIndex(ribbon)
- var gr = DATA.ribbons[r]
- rollImages(gr.length, ribbon)
- })
-
- .on('fittingImages', function(evt, n){
- //console.log('!!!! fittingImages')
- // load correct amount of images in each ribbon!!!
- var screen_size = getScreenWidthInImages()
- var gid = getImageGID()
-
- /* XXX used to skip ribbons that are not visible... (see bellow)
- var viewer = $('.viewer')
- var H = viewer.height()
- var h = getImage().height()
- */
-
- // update and align ribbons...
- $('.ribbon').each(function(){
- var r = $(this)
- /* XXX skip ribbons that are not visible...
- * causes misaligns and misloads on zoom-in...
- // NOTE: we factor in the scale difference to predict
- // ribbon position in the new view...
- var t = getRelativeVisualPosition(viewer, r).top * (n/screen_size)
- if( t+h <= 0 || t >= H ){
- console.log('#### skipping align of ribbon:', getRibbonIndex(r))
- return
- }
- */
- loadImagesAround(Math.round(screen_size * LOAD_SCREENS), gid, r, null, true)
- })
-
- centerView(null, 'css')
-
- // update settings...
- if(toggleSingleImageMode('?') == 'on'){
- SETTINGS['single-image-mode-screen-images'] = n
- } else {
- SETTINGS['ribbon-mode-screen-images'] = n
- }
-
- // update proportions...
- if(window.PROPORTIONS_RATIO_THRESHOLD != null
- && toggleSingleImageMode('?') == 'on'){
-
- var h = getVisibleImageSize('height')
- var w = getVisibleImageSize('width')
- var H = $('.viewer').innerHeight()
- var W = $('.viewer').innerWidth()
-
- var m = Math.min(W/w, H/h)
-
- if(m < PROPORTIONS_RATIO_THRESHOLD){
- toggleImageProportions('fit-viewer')
- } else {
- toggleImageProportions('none')
- }
- }
-
- // update size classes...
- // XXX make thresholds global...
- if(n <= 2.5){
- $('.viewer')
- .removeClass('small')
- .addClass('large')
- } else if (n >= 6) {
- $('.viewer')
- .addClass('small')
- .removeClass('large')
- } else {
- $('.viewer')
- .removeClass('small')
- .removeClass('large')
- }
-
- // update previews...
- updateImages()
- })
-
-
- .on('focusingImage', function(evt, image){
- image = $(image)
- DATA.current = getImageGID(image)
-
- if(window.setWindowTitle != null){
- // XXX do we need to hide the extension...
- setWindowTitle(getImageFileName())
- //.split(/\.(jpg|jpeg|png|gif)$/)[0])
- }
- })
-
-
- // basic image manipulation...
- .on('rotatingLeft rotatingRight', function(evt, image){
- $(image).each(function(i, e){
- var img = $(this)
- var gid = getImageGID(img)
- var orientation = img.attr('orientation')
-
- // change the image orientation status and add to
- // updated list...
- IMAGES[gid].orientation = orientation
- imageUpdated(gid)
- })
- })
- .on('flippingVertical flippingHorizontal', function(evt, image){
- $(image).each(function(i, e){
- var img = $(this)
- var gid = getImageGID(img)
- var flip = getImageFlipState(img)
-
- IMAGES[gid].flipped = flip
- imageUpdated(gid)
- })
- })
- .on('resetToOriginalImage', function(evt, image){
- $(image).each(function(i, e){
- var img = $(this)
- var gid = getImageGID(img)
-
- IMAGES[gid].flipped = null
- IMAGES[gid].orientation = 0
-
- imageUpdated(gid)
- })
- })
-
-
- // caching...
- /* XXX this appears to actually make things slower...
- .on('reloadedRibbon updatedRibbon', function(evt, ribbon){
-
- window.DEBUG && console.log('>>> (ribbon:', getRibbonIndex(ribbon), ') Updating cache...')
-
- preCacheRibbonImages(ribbon)
- })
- */
-
- // info...
- .on('focusingImage',
- function(){
- showRibbonIndicator()
- })
- .on([
- 'focusedNextRibbon',
- 'focusedPrevRibbon'
- ].join(' '),
- function(){
- if(toggleSingleImageMode('?') == 'on'){
- flashRibbonIndicator()
- }
- })
- .on([
- 'rotatingLeft',
- 'rotateingRight',
- 'flippingVertical',
- 'flippingHorizontal'
- ].join(' '),
- function(evt, image){
- updateGlobalImageInfo($(image))
- })
- .on([
- 'focusingImage',
- 'togglingMark',
- 'togglingBookmark',
- 'removeingAllMarks',
- 'removeingRibbonMarks',
- 'markingAll',
- 'markingRibbon',
- 'invertingMarks'
- ].join(' '),
- function(){
- updateGlobalImageInfo()
- updateContextIndicators()
- })
-
-
- .on('baseURLChanged', function(evt, url){
- saveLocalStorageBaseURL()
- saveLocalStorageBaseURLHistory()
- })
}
diff --git a/ui/ui.js b/ui/ui.js
index 61831153..0b2efd22 100755
--- a/ui/ui.js
+++ b/ui/ui.js
@@ -1261,5 +1261,29 @@ function showImageInfo(){
+/*********************************************************************/
+
+function setupUI(viewer){
+ console.log('UI: setup...')
+
+ return viewer
+ .click(function(){
+ if($('.ribbon').length == 0){
+ loadDirectoryDialog()
+ }
+ })
+ .on([
+ 'focusingImage',
+ 'fittingImages'
+ ].join(' '),
+ function(){
+ updateCurrentMarker()
+ })
+
+}
+SETUP_BINDINGS.push(setupUI)
+
+
+
/**********************************************************************
* vim:set ts=4 sw=4 nowrap : */