Lumiera  0.pre.03
»edit your freedom«
body-canvas-widget.cpp
Go to the documentation of this file.
1 /*
2  BodyCanvasWidget - custom drawing canvas to display the timeline body
3 
4  Copyright (C) Lumiera.org
5  2016, Hermann Vosseler <Ichthyostega@web.de>
6 
7  This program is free software; you can redistribute it and/or
8  modify it under the terms of the GNU General Public License as
9  published by the Free Software Foundation; either version 2 of
10  the License, or (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with this program; if not, write to the Free Software
19  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 
21 * *****************************************************/
22 
23 
48 #include "stage/gtk-base.hpp"
53 #include "stage/style-scheme.hpp"
54 
55 #include "common/advice.hpp"
56 #include "lib/util.hpp"
57 
58 #include <utility>
59 
60 
61 using lib::time::Time;
62 using util::max;
63 using util::isnil;
64 using Gdk::Rectangle;
65 using std::string;
66 using std::move;
67 
68 
69 
70 namespace stage {
71 namespace timeline {
72 
73  using CairoC = PCairoContext const&;
74  using StyleC = PStyleContext const&;
75 
76  namespace { // details of track background painting
77 
78  const int INITIAL_TIMERULER_HEIGHT_px = 30;
79  const int INITIAL_CONTENT_HEIGHT_px = 100;
80  const int MINIMAL_CONTENT_WIDTH_px = 100;
81 
84  lumiera::advice::Request<PStyleContext> trackRulerStyle{"style(trackRuler)"};
85 
86  cuString
87  slopeClassName(int depth)
88  {
89  switch (depth)
90  {
91  case 1: return CLASS_slope_deep1;
92  case 2: return CLASS_slope_deep2;
93  case 3: return CLASS_slope_deep3;
94  case 4: return CLASS_slope_deep4;
95  default:return CLASS_slope_verydeep;
96  }
97  }
98  const uint SLOPE_CAP_DEPTH = 5;
99 
100 
106  void
108  {
109  static bool styleSetupDone{false};
110  if (styleSetupDone) return;
111 
112  StyleC styleRuler{trackRulerStyle.getAdvice()};
113  StyleC styleBody {trackBodyStyle.getAdvice()};
114 
115  TrackBody::decoration.ruler = styleRuler->get_margin().get_top()
116  + styleRuler->get_margin().get_bottom()
117  + styleRuler->get_border().get_top()
118  + styleRuler->get_border().get_bottom()
119  + styleRuler->get_padding().get_top()
120  + styleRuler->get_padding().get_bottom()
121  ;
122  TrackBody::decoration.content = styleBody->get_margin().get_top()
123  + styleBody->get_margin().get_bottom()
124  + styleBody->get_padding().get_top()
125  + styleBody->get_padding().get_bottom()
126  ;
127  TrackBody::decoration.trackPad = styleBody->get_margin().get_top()
128  + styleBody->get_padding().get_top()
129  ;
130  TrackBody::decoration.topMar = styleBody->get_margin().get_top();
131  TrackBody::decoration.botMar = styleBody->get_margin().get_bottom();
132 
133  for (uint depth=SLOPE_CAP_DEPTH; depth>0; --depth)
134  {
135 // styleBody->context_save(); // <<<---does not work. Asked on SO: https://stackoverflow.com/q/57342478
136  styleBody->add_class (slopeClassName(depth));
137 
138  TrackBody::decoration.borders[depth] = styleBody->get_border().get_bottom();
139  TrackBody::decoration.borders[0] = styleBody->get_border().get_top(); // Note: we use a common size for all opening borders
140 
141  styleBody->remove_class (slopeClassName(depth));
142 // styleBody->context_restore(); // <<<---does not work...
143  }
144  styleSetupDone = true;
145  }
146 
147 
163  : public ProfileInterpreter
164  {
165  protected:
166  CairoC cox_;
167  StyleC style_; // CSS style for the main track body
168  StyleC styleR_; // CSS style for the an overview ruler
169  PixSpan visible_; // vertical extension of the timeline
170 
173  int line_ = 0;
174 
175  void
176  fillBackground (StyleC style, int height)
177  {
178  style->render_background (cox_
179  ,visible_.b // left start of the rectangle
180  ,line_ // top of the rectangle
181  ,visible_.delta() // width of the area
182  ,height // height to fill
183  );
184  }
185 
186  public:
187  AbstractTrackRenderer (CairoC currentDrawContext, DisplayManager& layout)
188  : cox_{currentDrawContext}
189  , style_{trackBodyStyle.getAdvice()}
190  , styleR_{trackRulerStyle.getAdvice()}
191  , visible_{layout.getPixSpan()}
192  { }
193  /*
194  * Note: we store a const& to the advice in the member fields.
195  * This is potentially dangerous, but seems adequate here, since
196  * the renderer instance does not outlive the BodyCanvasWidget,
197  * and the style context accessed through the advice is created
198  * once, at application startup. Please take into account that
199  * this drawing code is invoked very frequently from GUI thread.
200  */
201  };
202 
203 
205  : public AbstractTrackRenderer
206  {
207 
209  void
210  prelude() override
211  {
212  int topMargin = style_->get_margin().get_top();
213  line_ += topMargin;
214  }
215 
218  void
219  coda (uint pad) override
220  {
221  int bottomPad = pad + style_->get_margin().get_bottom();
222  line_ += bottomPad;
223  }
224 
227  void
228  ruler (uint contentHeight) override
229  {
230  int marTop = styleR_->get_margin().get_top();
231  int marBot = styleR_->get_margin().get_bottom();
232  int padTop = styleR_->get_padding().get_top();
233  int padBot = styleR_->get_padding().get_bottom();
234  int frameT = styleR_->get_border().get_top();
235  int frameB = styleR_->get_border().get_bottom();
236 
237  int heightWithFrame = contentHeight + padTop+padBot + frameT+frameB;
238 
239  line_ += marTop;
240  fillBackground(styleR_, heightWithFrame);
241  styleR_->render_frame (cox_
242  ,visible_.b
243  ,line_
244  ,visible_.delta()
245  ,heightWithFrame
246  );
247  line_ += heightWithFrame;
248  line_ += marBot;
249  }
250 
252  void
253  gap (uint h) override
254  {
255  line_ += h;
256  }
257 
260  void
261  content (uint contentHeight) override
262  {
263  int marTop = style_->get_margin().get_top();
264  int marBot = style_->get_margin().get_bottom();
265  int padTop = style_->get_padding().get_top();
266  int padBot = style_->get_padding().get_bottom();
267  int heightWithPadding = contentHeight + padTop+padBot;
268 
269  line_ += marTop;
270  fillBackground (style_, heightWithPadding);
271  line_ += heightWithPadding;
272  line_ += marBot;
273  }
274 
277  void
278  open() override
279  {
280 // style_->context_save(); // <<<---does not work. Asked on SO: https://stackoverflow.com/q/57342478
281  style_->add_class (slopeClassName (1));
282  int slopeWidth = style_->get_border().get_top();
283  style_->render_frame_gap(cox_
284  ,visible_.b - slopeWidth
285  ,line_
286  ,visible_.delta() + 2*slopeWidth
287  ,2*slopeWidth
288  //_______________________________we only need the top side of the frame
289  ,Gtk::PositionType::POS_BOTTOM
290  ,visible_.b
291  ,visible_.e + 2*slopeWidth
292  );
293 // style_->context_restore(); // <<<---does not work...
294  style_->remove_class (slopeClassName(1));
295  line_ += slopeWidth;
296  }
297 
304  void
305  close (uint n) override
306  {
307 // style_->context_save(); // <<<---does not work. Asked on SO: https://stackoverflow.com/q/57342478
308  style_->add_class (slopeClassName(n));
309  int slopeWidth = style_->get_border().get_bottom();
310  line_ -= slopeWidth; // set back to create room for the (invisible) top side of the frame
311  style_->render_frame_gap(cox_
312  ,visible_.b - slopeWidth
313  ,line_
314  ,visible_.delta() + 2*slopeWidth
315  ,2*slopeWidth
316  //_______________________________we only need the bottom side of the frame
317  ,Gtk::PositionType::POS_TOP
318  ,visible_.b
319  ,visible_.e + 2*slopeWidth
320  );
321 // style_->context_restore(); // <<<---does not work...
322  style_->remove_class (slopeClassName(n));
323  line_ += 2*slopeWidth;
324  }
325 
326  public:
327  using AbstractTrackRenderer::AbstractTrackRenderer;
328  };
329 
330 
331 
333  : public AbstractTrackRenderer
334  {
335 
337  void
338  prelude() override
339  {
340  /* nothing to paint */
341  line_ += style_->get_margin().get_top();
342  }
343 
346  void
347  coda (uint pad) override
348  {
349  /* nothing to paint */
350  line_ += pad + style_->get_margin().get_bottom();
351  }
352 
355  void
356  ruler (uint contentHeight) override
357  {
358  int marTop = styleR_->get_margin().get_top();
359  int marBot = styleR_->get_margin().get_bottom();
360  int padTop = styleR_->get_padding().get_top();
361  int padBot = styleR_->get_padding().get_bottom();
362  int frameT = styleR_->get_border().get_top();
363  int frameB = styleR_->get_border().get_bottom();
364 
365  int heightWithFrame = contentHeight + padTop+padBot + frameT+frameB;
366 
367  /* nothing to paint */
368  line_ += marTop
369  + heightWithFrame
370  + marBot;
371  }
372 
374  void
375  gap (uint h) override
376  {
377  /* nothing to paint */
378  line_ += h;
379  }
380 
384  void
385  content (uint contentHeight) override
386  {
387  int marTop = style_->get_margin().get_top();
388  int marBot = style_->get_margin().get_bottom();
389  int padTop = style_->get_padding().get_top();
390  int padBot = style_->get_padding().get_bottom();
391  int heightWithPadding = contentHeight + padTop+padBot;
392 
393  /* nothing to paint */
394  line_ += marTop
395  + heightWithPadding
396  + marBot;
397  }
398 
400  void
401  open() override
402  {
403 // style_->context_save(); // <<<---does not work. Asked on SO: https://stackoverflow.com/q/57342478
404  style_->add_class (slopeClassName (1));
405  int slopeWidth = style_->get_border().get_top();
406 // style_->context_restore(); // <<<---does not work...
407  style_->remove_class (slopeClassName(1));
408  line_ += slopeWidth;
409  }
410 
412  void
413  close (uint n) override
414  {
415 // style_->context_save(); // <<<---does not work. Asked on SO: https://stackoverflow.com/q/57342478
416  style_->add_class (slopeClassName(n));
417  int slopeWidth = style_->get_border().get_bottom();
418 // style_->context_restore(); // <<<---does not work...
419  style_->remove_class (slopeClassName(n));
420  line_ += slopeWidth;
421  }
422 
423  public:
424  using AbstractTrackRenderer::AbstractTrackRenderer;
425  };
426 
427 
428  template<class PINT, bool isRuler>
429  auto
430  makeRenderer (DisplayManager& layout, BodyCanvasWidget::ProfileGetter& getProfile)
431  {
432  return [&](CairoC cox)
433  {
434  PINT concreteRenderScheme{cox, layout};
435  getProfile().performWith (concreteRenderScheme, isRuler);
436  };
437  }
438 
439  // abbreviations for readability
442  const bool RULER = true;
443  const bool BODY = false;
444  }
445 
446 
447 
448 
449 
450  BodyCanvasWidget::~BodyCanvasWidget() { }
451 
452 
453  BodyCanvasWidget::BodyCanvasWidget (DisplayManager& displayManager)
454  : Glib::ObjectBase("body") // enables use of custom CSS properties (on 'gtkmm__CustomObject_body')
455  , Gtk::Box{Gtk::ORIENTATION_VERTICAL}
456  , layout_{displayManager}
457  , profile_{}
458  , rootBody_{nullptr}
459  , contentArea_{}
460  , rulerArea_{contentArea_.get_hadjustment(), Gtk::Adjustment::create (0,0,0,0,0,0)}
461  , rulerCanvas_{makeRenderer<Grounding,RULER>(layout_,getProfile), makeRenderer<Overlay,RULER>(layout_,getProfile)}
462  , mainCanvas_ {makeRenderer<Grounding,BODY> (layout_,getProfile), makeRenderer<Overlay,BODY> (layout_,getProfile)}
463  {
464  get_style_context()->add_class(CLASS_timeline);
465  get_style_context()->add_class(CLASS_timeline_body);
466 
467  // respond to any structure changes of the timeline by recomputing the TrackProfile
468  layout_.signalStructureChange_.connect (sigc::mem_fun (*this, &BodyCanvasWidget::slotStructureChange));
469 
470  // on demand access and possible (re)establish the current "profile" of the tracks for drawing...
471  getProfile = [this]() -> TrackProfile&
472  {
473  maybeRebuildLayout();
474  return profile_;
475  };
476 
477  // initially set up some dummy space. Will be updated to match on first draw() call...
478  adjustCanvasSize(MINIMAL_CONTENT_WIDTH_px, INITIAL_CONTENT_HEIGHT_px, INITIAL_TIMERULER_HEIGHT_px);
479 
480  this->set_border_width (0);
481  this->property_expand() = true; // dynamically grab any available additional space
482  this->pack_start (rulerArea_, Gtk::PACK_SHRINK);
483  this->pack_start (contentArea_, Gtk::PACK_EXPAND_WIDGET);
484 
485  rulerArea_.set_shadow_type (Gtk::SHADOW_NONE);
486  rulerArea_.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_NEVER);
487  rulerArea_.property_expand() = false;
488  rulerArea_.add (rulerCanvas_);
489  contentArea_.set_shadow_type (Gtk::SHADOW_NONE);
490  contentArea_.set_policy (Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS); // always use both scrollbars ////////////////TICKET #1208 : 2/2002 Gtk::POLICY_AUTOMATIC hides scrollbar after focus loss
491  contentArea_.property_expand() = true; // dynamically grab additional space
492  contentArea_.add (mainCanvas_);
493 
494  // realise all initially configured elements....
495  this->show_all();
496  }
497 
498 
499 
508  void
510  {
511  rootBody_ = &rootTrackBody;
512  }
513 
514  void
515  BodyCanvasWidget::disable()
516  {
517  profile_.clear();
518  rootBody_ = nullptr;
519  }
520 
521 
523  void
525  {
526  profile_.clear();
527  }
528 
529 
535  void
537  {
538  while (rootBody_ and isnil (profile_))
539  {
541  layout_.triggerDisplayEvaluation();
542  }
543  ENSURE (not isnil (profile_), "DisplayEvaluation logic broken");
544  }
545 
546 
547 
555  void
556  BodyCanvasWidget::adjustCanvasSize(int canvasWidth, int contentHeight, int rulerHeight)
557  {
558  auto adjust = [](Gtk::Layout& canvas, guint newWidth, guint newHeight) -> void
559  {
560  guint currWidth{0}, currHeight{0};
561  canvas.get_size(currWidth, currHeight);
562  if (currWidth != newWidth or currHeight != newHeight)
563  {
564  canvas.set_size(newWidth, newHeight);
565  // Note: must force GTK at least to claim the necessary height,
566  // otherwise the enclosing Box won't reflow and adapt;
567  // implicitly this defines minimum timeline window width
568  canvas.set_size_request(MINIMAL_CONTENT_WIDTH_px, newHeight);
569  }
570  };
571 
572  adjust (rulerCanvas_, canvasWidth, rulerHeight);
573  adjust (mainCanvas_, canvasWidth, contentHeight);
574  }
575 
576 
577  void
578  BodyCanvasWidget::forceRedraw()
579  {
580  rulerCanvas_.queue_draw();
581  mainCanvas_.queue_draw();
582  }
583 
585  BodyCanvasWidget::getCanvas (int yPos)
586  {
587  return mainCanvas_;
588  }
589 
590 
591  /* ==== Interface: CanvasHook ===== */
592 
593  void
594  BodyCanvasWidget::hook (Gtk::Widget& widget, int xPos, int yPos)
595  {
597  getCanvas(yPos).put (widget, xPos, yPos);
598  }
599 
600  void
601  BodyCanvasWidget::remove (Gtk::Widget& widget)
602  {
604  getCanvas(0).remove (widget);
605  }
606 
607  void
608  BodyCanvasWidget::move (Gtk::Widget& widget, int xPos, int yPos)
609  {
611  getCanvas(yPos).move (widget, xPos, yPos);
612  }
613 
616  {
617  return layout_;
618  }
619 
625  void
627  {
628  // Traverse TrackBody structure and populate the (track)profile
629  uint contentHeight = rootBody_->establishTrackSpace (profile_);
630  uint rulerHeight = rootBody_->calcRulerHeight();
631  adjustCanvasSize(layout_.getPixSpan().delta(), contentHeight, rulerHeight);
632 
634  }
635 
636  void
637  BodyCanvasWidget::completeLayout (DisplayEvaluation&)
638  { /* nothing to do */ }
639 
640 
641 
642 
643 
644 
645  TimelineCanvas::TimelineCanvas (_Renderer groundingFun, _Renderer overlayFun)
646  : Gtk::Layout{}
647  , renderGrounding_{groundingFun}
648  , renderOverlay_{overlayFun}
649  { }
650 
651 
661  bool
663  {
664  // draw track structure behind all widgets
665  openCanvas (cox);
666  drawGrounding (cox);
667  closeCanvas (cox);
668 
669  // cause child widgets to be redrawn
670  bool event_is_handled = Gtk::Layout::on_draw(cox);
671 
672  // draw dynamic markers and locators on top
673  openCanvas (cox);
674  drawOverlays (cox);
675  closeCanvas (cox);
676 
677  return event_is_handled;
678  }
679 
680 
688  void
690  {
691  auto adjH = get_hadjustment();
692  auto adjV = get_vadjustment();
693  double offH = adjH->get_value();
694  double offV = adjV->get_value();
695 
696  cox->save();
697  cox->translate(-offH, -offV);
698  }
699 
700 
705  void
707  {
708  cox->restore();
709  }
710 
711 
716  void
718  {
719  renderGrounding_(cox);
720  }
721 
722 
727  void
729  {
730  renderOverlay_(cox);
731  }
732 
733 
734 
735 }}// namespace stage::timeline
void close(uint n) override
paint closing slope to finish nested sub tracks
void maybeRebuildLayout()
Possibly (re)build the allocation and distribution of layout space.
PixSpan getPixSpan()
the overall horizontal pixel span to cover by this timeline
SignalStructureChange signalStructureChange_
signal to be invoked whenever the virtual structure of the corresponding timeline changes...
void closeCanvas(CairoC)
Finish and close the virtual drawing canvas established by openCanvas().
Access point for the advised entity (client).
Definition: advice.hpp:437
std::function< TrackProfile &()> ProfileGetter
a way to get and possibly (re)compute the current TrackProfile
Mix-in interface to allow for concrete CanvasHooked widgets to adapt themselves to the metric current...
Definition: canvas-hook.hpp:82
Widget to render the body of timeline display, by custom drawing into a canvas control.
void content(uint contentHeight) override
fill background of track content area with the given vertical extension
static Decoration decoration
storage for common style/padding settings
Definition: track-body.hpp:111
void ruler(uint contentHeight) override
draw grounding of an overview/ruler track with the given height
void open() override
render overlays covering the opening slope towards nested tracks
void coda(uint pad) override
finish painting overlays a the bottom of the track body area
void openCanvas(CairoC)
Prepare the drawing canvas to work within our virtual canvas coordinate system.
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:308
void prelude() override
overlays to show at top of the track body area
model::DisplayMetric & getMetric() const override
access the component to handle layout metric
void ruler(uint contentHeight) override
draw overlays on top of overview/ruler track
Visitor and state holder for a collaborative layout adjustment pass.
Expecting Advice and giving Advice: a cross-cutting collaboration of loosely coupled participants...
Borders borders
width of up to 6 levels of combined upward slope borders (defined in CSS)
Definition: track-body.hpp:79
Abstraction: service for the widgets to translate themselves into screen layout.
void content(uint contentHeight) override
place overlays on top of of track content area,
void prelude() override
create spacing at the top of the track body area
Description of the structure and arrangement of tracks for display in the UI.
void adjustCanvasSize(int canvasWidth, int totalHeight, int rulerHeight)
After the (recent) display evaluation pass has negotiated the required space for the currently presen...
Lumiera GTK UI implementation root.
Definition: guifacade.cpp:46
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
void drawGrounding(CairoC)
Establish and render the structure of (possibly nested) tracks and overview rulers.
void gap(uint h) override
insert additional padding/gap (typically below a ruler)
void installForkRoot(TrackBody &rootTrackBody)
The Lumiera Timeline model does not rely on a list of tracks, as most conventional video editing soft...
Helper to organise and draw the space allocated for a fork of sub-tracks.
Definition: track-body.hpp:95
This helper class serves to manage the layout and display of the horizontally extended space of a "tr...
virtual bool on_draw(CairoC) override
Custom drawing of the timeline content area.
void close(uint n) override
render overlays covering the closing slope towards nested tracks
Abstraction to build the layout for the track spaces within timeline display.
void establishLayout(DisplayEvaluation &) override
respond to the DisplayEvaluation pass.
Definition of access keys for uniform UI styling.
void slotStructureChange() noexcept
force rebuilding of theTrackProfile whenever the global timeline structure changes ...
A set of basic GTK includes for the UI.
void setupAdditionalTrackPadding_fromCSS()
Adjust the vertical space to accommodate for additional decorations as required by the CSS style rule...
Interface for coordination of the overall timeline display.
void open() override
paint opening slope to enter nested sub tracks
lumiera::advice::Request< PStyleContext > trackBodyStyle
request a pre-defined CSS style context for the track body