diff --git a/include/lgi/common/AudioView.h b/include/lgi/common/AudioView.h --- a/include/lgi/common/AudioView.h +++ b/include/lgi/common/AudioView.h @@ -1,1894 +1,1897 @@ /// Audio waveform display control #pragma once #include "lgi/common/Layout.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Menu.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Thread.h" #include "lgi/common/ThreadEvent.h" #include "lgi/common/DragAndDrop.h" #include "lgi/common/DropFiles.h" #include "lgi/common/PopupNotification.h" #include "ao.h" #define CC(code) LgiSwap32(code) #define ERROR_ZERO_SAMPLE_COUNT 10 struct int24_t { int32_t s : 24; }; union sample_t { int8_t *i8; int16_t *i16; int24_t *i24; int32_t *i32; float *f; sample_t(void *ptr = NULL) { *this = ptr; } sample_t &operator=(void *ptr) { i8 = (int8_t*)ptr; return *this; } }; template struct AvRect { T x1 = 0, y1 = 0, x2 = 0, y2 = 0; AvRect &operator =(const LRect &r) { x1 = r.x1; y1 = r.y1; x2 = r.x2; y2 = r.y2; return *this; } T X() { return x2 - x1 + 1; } T Y() { return y2 - y1 + 1; } bool Valid() { return x2 >= x1 && y2 >= y1; } operator LRect() { LRect r((int)x1, (int)y1, (int)x2, (int)y2); return r; } void ZOff(T sx, T sy) { x1 = y1 = 0; x2 = sx; y2 = sy; } const char *GetStr() { static char s[96]; sprintf_s(s, sizeof(s), LPrintfInt64 "," LPrintfInt64 "," LPrintfInt64 "," LPrintfInt64, x1, y1, x2, y2); return s; } AvRect &Offset(T x, T y) { x1 += x; x2 += x; y1 += y; y2 += y; return *this; } }; typedef int32_t (*ConvertFn)(sample_t &ptr); class LAudioView : public LLayout, public LThread, public LCancel, public LDragDropTarget { public: constexpr static int DefaultBorder = 10; // px enum LFileType { AudioUnknown, AudioRaw, AudioWav, AudioAif, }; enum LSampleType { AudioS8, AudioS16LE, AudioS16BE, AudioS24LE, AudioS24BE, AudioS32LE, AudioS32BE, AudioFloat32, }; enum Messages { M_WAVE_FORMS_FINISHED = M_USER + 100, M_SAVE_FINISHED, }; struct LBookmark { int x = 0; bool error = false; int64_t sample; LColour colour; }; protected: enum LCmds { IDC_COPY_CURSOR, IDC_LOAD_WAV, IDC_LOAD_RAW, IDC_SAVE_WAV, IDC_SAVE_RAW, IDC_DRAW_AUTO, IDC_DRAW_LINE, IDC_SCAN_SAMPLES, IDC_SCAN_GROUPS, IDC_44K, IDC_48K, IDC_88K, IDC_96K, IDC_MONO, IDC_STEREO, IDC_2pt1_CHANNELS, IDC_5pt1_CHANNELS, }; enum LDrawMode { DrawAutoSelect, DrawSamples, ScanSamples, ScanGroups, }; enum CtrlState { CtrlNormal, CtrlLoading, CtrlBuildingWaveforms, CtrlSaving, } State = CtrlNormal; LArray Audio; LFileType Type = AudioUnknown; LSampleType SampleType = AudioS16LE; uint32_t SampleBits = 0; uint32_t SampleRate = 0; uint32_t Channels = 0; size_t DataStart = 0; int64_t CursorSample = 0; AvRect Data; LArray> ChData; double XZoom = 1.0; LString Msg, ErrorMsg; LDrawMode DrawMode = DrawAutoSelect; LArray> Bookmarks; LString FilePath; template struct Grp { T Min = 0, Max = 0; }; LArray>> IntGrps; LArray>> FloatGrps; // Libao stuff int AoDriver = -1; bool AoPlaying = false; ao_device *AoDev = NULL; LThreadEvent AoEvent; void MouseToCursor(LMouse &m) { auto idx = ViewToSample(m.x); if (idx != CursorSample) { CursorSample = idx; UpdateMsg(); Invalidate(); } } template void SetData(T &r) { Data = r; ChData.Length(Channels); auto Dy = (int)Data.Y() - 1; for (uint32_t i=0; iMsg()) { case M_WAVE_FORMS_FINISHED: OnWaveFormsFinished(); break; } return LView::OnEvent(m); } int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) override { Formats.SupportsFileDrops(); return DROPEFFECT_COPY; } int OnDrop(LArray &Data, LPoint Pt, int KeyState) override { for (auto &d : Data) { if (d.IsFileDrop()) { LDropFiles df(d); auto w = GetWindow(); if (w) { w->OnReceiveFiles(df); return DROPEFFECT_COPY; } } } return DROPEFFECT_NONE; } bool Empty() { Audio.Free(); Type = AudioUnknown; SampleType = AudioS16LE; SampleRate = 0; SampleBits = 0; DataStart = 0; Channels = 0; CursorSample = 0; Data.ZOff(-1, -1); ChData.Empty(); Msg.Empty(); FilePath.Empty(); IntGrps.Empty(); Bookmarks.Empty(); if (AoPlaying) { AoPlaying = false; LSleep(50); } if (AoDev) { ao_close(AoDev); AoDev = NULL; } Invalidate(); return false; } union AifType { uint32_t id; char str[4]; AifType(uint32_t init) { id = init; } bool Is(uint32_t atom) { return LgiSwap32(id) == atom; } }; bool ParseAif() { LPointer p; p.s8 = Audio.AddressOf(); auto end = p.s8 + Audio.Length(); while (p.s8 < end) { AifType Id = *p.u32++; auto Sz = *p.u32++; Sz = LgiSwap32(Sz); // printf("Id='%4.4s' Sz=%i\n", &Id, Sz); if (Id.Is('FORM')) { Id = *p.u32++; if (!Id.Is('AIFF')) return false; } else if (Id.Is('COMT')) { p.u8 += Sz + (Sz % 2); } else if (Id.Is('CHAN')) { p.u8 += Sz + (Sz % 2); } else if (Id.Is('COMM')) { auto channels = *p.u16++; Channels = LgiSwap16(channels); p.u32++; // long numSampleFrames uint16_t bits = *p.u16++; SampleBits = LgiSwap16(bits); uint32_t exp = ((int)p.u8[0]<<8) + p.u8[1]; //first 16 bits exp = exp - 0x3fff; uint32_t man = ((int)p.u8[2] << 24) + ((int)p.u8[3] << 16) + ((int)p.u8[4] << 8) + p.u8[5]; //bits 16..47 SampleRate = (uint32_t) (man >> (0x1f - exp)); p.s8 += 10; if (SampleBits == 16) SampleType = AudioS16BE; else if (SampleBits == 24) SampleType = AudioS24BE; else if (SampleBits == 32) SampleType = AudioS32BE; printf("Channels=%i, Bits=%i, Rate=%i\n", Channels, SampleBits, SampleRate); } else if (Id.Is('SSND')) { p.u32 += 2; DataStart = p.s8 - Audio.AddressOf(); return true; } else { printf("%s:%i - Unexpected atom '%4.4s'\n", _FL, Id.str); return false; } } return false; } bool ParseWav() { // Parse the wave file... LPointer p; p.s8 = Audio.AddressOf(); auto end = p.s8 + Audio.Length(); if (*p.u32++ != CC('RIFF')) return Empty(); auto ChunkSz = *p.u32++; auto Fmt = *p.u32++; while (p.s8 < end) { auto SubChunkId = *p.u32++; auto SubChunkSz = *p.u32++; auto NextChunk = p.u8 + SubChunkSz; if (SubChunkId == CC('fmt ')) { auto AudioFmt = *p.u16++; Channels = *p.u16++; SampleRate = *p.u32++; auto ByteRate = *p.u32++; auto BlockAlign = *p.u16++; SampleBits = *p.u16++; if (SampleBits == 16) SampleType = AudioS16LE; else if (SampleBits == 24) SampleType = AudioS24LE; else if (SampleBits == 32) SampleType = AudioS32LE; printf("Channels=%i\n", Channels); printf("SampleRate=%i\n", SampleRate); printf("SampleBits=%i\n", SampleBits); } else if (SubChunkId == CC('data')) { DataStart = p.s8 - Audio.AddressOf(); printf("DataStart=" LPrintfSizeT "\n", DataStart); break; } p.u8 = NextChunk; } if (!DataStart) { ErrorMsg = "No 'data' element found."; LPopupNotification::Message(GetWindow(), ErrorMsg); return Empty(); } return true; } bool Load(const char *FileName, int rate = 0, int bitDepth = 0, int channels = 0) { LFile f(FileName, O_READ); if (!f.IsOpen()) { ErrorMsg.Printf("Can't open '%s' for reading.", FileName); return false; } Empty(); Msg.Printf("Loading '%s'...", FileName); LPopupNotification::Message(GetWindow(), Msg); FilePath = FileName; if (!Audio.Length(f.GetSize())) { ErrorMsg.Printf("Can't allocate %s.", LFormatSize(f.GetSize()).Get()); return Empty(); } auto rd = f.Read(Audio.AddressOf(), f.GetSize()); if (rd > 0 && rd != f.GetSize()) Audio.Length(rd); auto Ext = LGetExtension(FileName); if (!Stricmp(Ext, "wav")) Type = AudioWav; else if (!Stricmp(Ext, "aif")) Type = AudioAif; else if (!Stricmp(Ext, "raw")) Type = AudioRaw; else { ErrorMsg.Printf("Unknown format: %s", Ext); LPopupNotification::Message(GetWindow(), ErrorMsg); return Empty(); } if (rate) SampleRate = rate; DataStart = 0; SampleBits = bitDepth; Channels = channels; if (bitDepth == 16) { SampleType = AudioS16LE; } else if (bitDepth == 32) { SampleType = AudioS32LE; } else if (Type == AudioWav) { if (!ParseWav()) return false; } else if (Type == AudioAif) { if (!ParseAif()) return false; } else if (Type == AudioRaw) { // Assume 32bit SampleType = AudioS32LE; SampleBits = 32; } if (!SampleRate) SampleRate = 44100; if (!Channels) Channels = 2; Invalidate(); LPopupNotification::Message(GetWindow(), "Loaded ok..."); return true; } protected: struct SaveThread : public LThread { LAudioView *view; LString ErrorMsg; LString FileName; SaveThread(LAudioView *v, const char *filename) : LThread("SaveThread", v->AddDispatch()) { view = v; FileName = filename; LString Msg; Msg.Printf("Saving '%s'...", FileName.Get()); LPopupNotification::Message(view->GetWindow(), Msg); Run(); } ~SaveThread() { WaitForExit(); } void OnComplete() { if (ErrorMsg) LPopupNotification::Message(view->GetWindow(), ErrorMsg); else LPopupNotification::Message(view->GetWindow(), "Saved ok."); view->State = CtrlNormal; view->Saving.Release(); // it doesn't own the ptr anymore DeleteOnExit = true; } int Main() { LFile f; if (!f.Open(FileName, O_WRITE)) { ErrorMsg.Printf("Can't open '%s' for writing.", FileName.Get()); return -1; } f.SetSize(0); auto wr = f.Write(view->Audio.AddressOf(0), view->Audio.Length()); if (wr != view->Audio.Length()) { ErrorMsg.Printf("Write error: only wrote " LPrintfSizeT " of " LPrintfSizeT " bytes", wr, view->Audio.Length()); return -1; } return 0; } }; LAutoPtr Saving; void Save(const char *FileName = NULL) { if (!FileName) FileName = FilePath; if (!FileName) return; Saving.Reset(new SaveThread(this, FileName)); } LRect DefaultPos() { auto c = GetClient(); c.y1 += LSysFont->GetHeight(); c.Inset(DefaultBorder, DefaultBorder); return c; } size_t GetSamples() { + if (Audio.Length() == 0 || SampleBits == 0 || Channels == 0) + return 0; + return (Audio.Length() - DataStart) / (SampleBits >> 3) / Channels; } int SampleToView(size_t idx) { return (int) (Data.x1 + ((idx * Data.X()) / GetSamples())); } size_t ViewToSample(int x /*px*/) { int64_t offset = (int64_t)x - (int64_t)Data.x1; int64_t samples = GetSamples(); int64_t dx = (int64_t)Data.x2 - (int64_t)Data.x1 + 1; double pos = (double) offset / dx; int64_t idx = (int64_t)(samples * pos); #if 0 printf("ViewToSample(%i) data=%s offset=" LPrintfInt64 " samples=" LPrintfInt64 " idx=" LPrintfInt64 " pos=%f\n", x, Data.GetStr(), offset, samples, idx, pos); #endif if (idx < 0) idx = 0; else if (idx >= samples) idx = samples - 1; return idx; } void OnPosChange() override { auto def = DefaultPos(); LRect d = Data; d.y1 = def.y1; d.y2 = def.y2; SetData(d); } #define CheckItem(Sub, Name, Id, Chk) \ { \ auto item = Sub->AppendItem(Name, Id); \ if (item && Chk) item->Checked(true); \ } void OnMouseClick(LMouse &m) override { if (m.IsContextMenu()) { LSubMenu s; s.AppendItem("Copy Cursor Address", IDC_COPY_CURSOR); s.AppendSeparator(); auto File = s.AppendSub("File"); File->AppendItem("Load Wav", IDC_LOAD_WAV); File->AppendItem("Load Raw", IDC_LOAD_RAW); File->AppendItem("Save Wav", IDC_SAVE_WAV); File->AppendItem("Save Raw", IDC_SAVE_RAW); auto Draw = s.AppendSub("Draw Mode"); CheckItem(Draw, "Auto", IDC_DRAW_AUTO, DrawMode == DrawAutoSelect); CheckItem(Draw, "Line", IDC_DRAW_LINE, DrawMode == DrawSamples); CheckItem(Draw, "Scan Samples", IDC_SCAN_SAMPLES, DrawMode == ScanSamples); CheckItem(Draw, "Scan Groups", IDC_SCAN_GROUPS, DrawMode == ScanGroups); auto Rate = s.AppendSub("Sample Rate"); CheckItem(Rate, "44.1k", IDC_44K, SampleRate == 44100); CheckItem(Rate, "48k", IDC_48K, SampleRate == 4800); CheckItem(Rate, "88.2k", IDC_88K, SampleRate == 44100*2); CheckItem(Rate, "96k", IDC_96K, SampleRate == 48000*2); auto Ch = s.AppendSub("Channels"); CheckItem(Ch, "Mono", IDC_MONO, Channels == 1); CheckItem(Ch, "Stereo", IDC_STEREO, Channels == 2); CheckItem(Ch, "2.1", IDC_2pt1_CHANNELS, Channels == 3); CheckItem(Ch, "5.1", IDC_5pt1_CHANNELS, Channels == 6); bool Update = false; switch (s.Float(this, m)) { case IDC_COPY_CURSOR: { LClipBoard clip(this); size_t addrVal = DataStart + (CursorSample * Channels * SampleBits / 8); LString addr; addr.Printf(LPrintfSizeT, addrVal); clip.Text(addr); break; } case IDC_SAVE_RAW: { auto s = new LFileSelect; s->Parent(this); s->Save([this](auto s, auto ok) { if (ok) { LFile out(s->Name(), O_WRITE); if (out) { out.SetSize(0); auto ptr = Audio.AddressOf(DataStart); out.Write(ptr, Audio.Length() - DataStart); } else LgiMsg(this, "Can't open '%s' for writing.", "Error", MB_OK, s->Name()); } delete s; }); break; } case IDC_DRAW_AUTO: DrawMode = DrawAutoSelect; Update = true; break; case IDC_DRAW_LINE: DrawMode = DrawSamples; Update = true; break; case IDC_SCAN_SAMPLES: DrawMode = ScanSamples; Update = true; break; case IDC_SCAN_GROUPS: DrawMode = ScanGroups; Update = true; break; case IDC_44K: SampleRate = 44100; Update = true; break; case IDC_48K: SampleRate = 48000; Update = true; break; case IDC_88K: SampleRate = 44100*2; Update = true; break; case IDC_96K: SampleRate = 48000*2; Update = true; break; case IDC_MONO: Channels = 1; Update = true; break; case IDC_STEREO: Channels = 2; Update = true; break; case IDC_2pt1_CHANNELS: Channels = 3; Update = true; break; case IDC_5pt1_CHANNELS: Channels = 6; Update = true; break; default: break; } if (Update) { IntGrps.Length(0); FloatGrps.Length(0); SetData(Data); Invalidate(); } } else if (m.Left()) { Focus(true); MouseToCursor(m); } } void OnMouseMove(LMouse &m) override { if (m.Down()) MouseToCursor(m); } bool OnKey(LKey &k) override { // k.Trace("key"); if (k.IsChar) { switch (k.c16) { case ' ': { if (k.Down()) TogglePlay(); return true; } } } else if (k.Ctrl()) { if (k.c16 == 'S') { if (k.Down()) Save(); return true; } else if (k.c16 == 'W') { if (k.Down()) Empty(); return true; } } return false; } sample_t AddressOf(size_t Sample) { int SampleBytes = Channels * (SampleBits >> 3); return sample_t(Audio.AddressOf(DataStart + (Sample * SampleBytes))); } int64_t PtrToSample(sample_t &ptr) { int sampleBytes = SampleBits >> 3; auto start = Audio.AddressOf(DataStart); return (ptr.i8 - start) / (sampleBytes * Channels); } void UpdateMsg() { ssize_t Samples = GetSamples(); auto Addr = AddressOf(CursorSample); LString::Array Val; size_t GraphLen = IntGrps.Length() ? IntGrps[0].Length() : 0; auto Convert = GetConvert(); // Work out the time stamp of the cursor: auto Seconds = CursorSample / SampleRate; auto Hours = Seconds / 3600; Seconds -= Hours * 3600; auto Minutes = Seconds / 60; Seconds -= Minutes * 60; auto Ms = (CursorSample % SampleRate) * 1000 / SampleRate; LString Time; Time.Printf("%i:%02.2i:%02.2i.%i", (int)Hours, (int)Minutes, (int)Seconds, (int)Ms); for (uint32_t ch=0; ch cli.x1) d.Offset(cli.x1 - d.x1, 0); else if (d.x2 < cli.x2) d.Offset(cli.x2 - d.x2, 0); SetData(d); Invalidate(); } else { auto Client = GetClient(); int change = (int)(Lines * -6); auto d = Data; d.x1 += change; d.x2 += change; if (d.x2 < Client.x1) d.Offset(-d.x2, 0); else if (d.x1 >= Client.x2) d.Offset(-(d.x2 - Client.x2), 0); SetData(d); Invalidate(); } UpdateMsg(); return true; } #define PROFILE_PAINT_SAMPLES 0 #if PROFILE_PAINT_SAMPLES #define PROF(name) Prof.Add(name) #else #define PROF(name) #endif ConvertFn GetConvert() { switch (SampleType) { case AudioS8: return [](sample_t &s) -> int32_t { return *s.i8++; }; case AudioS16LE: return [](sample_t &s) -> int32_t { return *s.i16++; }; case AudioS16BE: return [](sample_t &s) -> int32_t { int16_t i = LgiSwap16(*s.i16); s.i16++; return i; }; case AudioS24LE: return [](sample_t &s) -> int32_t { int32_t i = s.i24->s; s.i8 += 3; return i; }; case AudioS24BE: return [](sample_t &s) -> int32_t { int24_t i; int8_t *p = (int8_t*)&i; p[0] = s.i8[2]; p[1] = s.i8[1]; p[2] = s.i8[0]; s.i8 += 3; return i.s; }; case AudioS32LE: return [](sample_t &s) -> int32_t { return *s.i32++; }; case AudioS32BE: return [](sample_t &s) -> int32_t { int32_t i = *s.i32++; return LgiSwap32(i); }; default: LAssert(!"Not impl."); break; } return NULL; } private: constexpr static int WaveformBlockSize = 256; struct WaveFormWork { ConvertFn convert; sample_t start; int8_t *end = NULL; int stepBytes = 0; int blkBytes = 0; size_t blocks = 0; Grp *out = NULL; }; LArray WaveFormTodo; LArray WaveFormThreads; struct WaveFormThread : public LThread, public LCancel { LAudioView *view; WaveFormThread(LAudioView *v) : LThread("WaveFormThread", v->AddDispatch()) { view = v; Run(); } ~WaveFormThread() { Cancel(); } void OnComplete() { // Clean up thread array... LThread *t = this; LAssert(view->WaveFormThreads.HasItem(t)); view->WaveFormThreads.Delete(t); DeleteOnExit = true; // Last one finished? if (view->WaveFormThreads.Length() == 0) view->PostEvent(M_WAVE_FORMS_FINISHED); } int Main() { while (!IsCancelled()) { WaveFormWork *w = NULL; if (view->Lock(_FL, 500)) { // Is there work? if (view->WaveFormTodo.Length() > 0) { w = view->WaveFormTodo[0]; view->WaveFormTodo.DeleteAt(0); } view->Unlock(); if (!w) return 0; } auto s = w->start; auto block = w->out; auto convert = w->convert; // For all blocks we're assigned... for (int b = 0; b < w->blocks && !IsCancelled(); b++, s.i8 += w->blkBytes, block++) { auto ptr = s; auto blkEnd = s.i8 + w->blkBytes; auto stepBytes = w->stepBytes; // For all samples in the block... block->Min = block->Max = convert(ptr); // init min/max with first sample while (ptr.i8 < blkEnd) { auto n = convert(ptr); if (n < block->Min) block->Min = n; if (n > block->Max) block->Max = n; ptr.i8 += stepBytes; } } } return 0; } }; public: void BuildWaveForms(LArray>> *Grps) { LPopupNotification::Message(GetWindow(), "Building waveform data..."); State = CtrlBuildingWaveforms; auto SampleBytes = SampleBits / 8; auto start = Audio.AddressOf(DataStart); size_t samples = GetSamples(); // auto end = start + (SampleBytes * samples * Channels); auto BlkBytes = WaveformBlockSize * Channels * SampleBytes; auto Blocks = ((Audio.Length() - DataStart) + BlkBytes - 1) / BlkBytes; int Threads = LAppInst->GetCpuCount(); // Initialize the graph Grps->Length(Channels); for (uint32_t ch = 0; ch < Channels; ch++) { auto &GraphArr = (*Grps)[ch]; GraphArr.Length(Blocks); for (int Thread = 0; Thread < Threads; Thread++) { auto from = Thread * Blocks / Threads; auto to = (Thread + 1) * Blocks / Threads; WaveFormWork *work = new WaveFormWork; work->convert = GetConvert(); work->start.i8 = start + (from * BlkBytes) + (ch * SampleBytes); work->end = start + (to * BlkBytes) + (ch * SampleBytes); work->stepBytes = (Channels - 1) * SampleBytes; work->blkBytes = BlkBytes; work->blocks = to - from; work->out = GraphArr.AddressOf(from); WaveFormTodo.Add(work); } } for (int Thread = 0; Thread < Threads; Thread++) WaveFormThreads.Add(new WaveFormThread(this)); } void OnWaveFormsFinished() { State = CtrlNormal; LPopupNotification::Message(GetWindow(), "Done."); Invalidate(); } void PaintSamples(LSurface *pDC, int ChannelIdx, LArray>> *Grps) { #if PROFILE_PAINT_SAMPLES LProfile Prof("PaintSamples", 10); #endif int32_t MaxT = 0; ConvertFn Convert = GetConvert(); if (SampleBits == 8) MaxT = 0x7f; else if (SampleBits == 16) MaxT = 0x7fff; else if (SampleBits == 24) MaxT = 0x7fffff; else MaxT = 0x7fffffff; auto SampleBytes = SampleBits / 8; auto Client = GetClient(); auto &c = ChData[ChannelIdx]; auto start = Audio.AddressOf(DataStart); size_t samples = GetSamples(); // printf("samples=" LPrintfSizeT "\n", samples); // auto end = start + (SampleBytes * samples * Channels); auto cy = c.y1 + (c.Y() / 2); pDC->Colour(cGrid); LRect cr = c; pDC->Box(&cr); if (Grps->Length() == 0) { BuildWaveForms(Grps); return; } else if (State != CtrlNormal) { return; } PROF("PrePaint"); auto YScale = c.Y() / 2; int CursorX = SampleToView(CursorSample); double blkScale = Grps->Length() > 0 ? (double) (*Grps)[0].Length() / c.X() : 1.0; // printf("blkScale=%f blks/px\n", blkScale); LDrawMode EffectiveMode = DrawMode; auto StartSample = ViewToSample(Client.x1); auto EndSample = ViewToSample(Client.x2); auto SampleRange = EndSample - StartSample; if (EffectiveMode == DrawAutoSelect) { if (SampleRange < Client.X() * 32) EffectiveMode = DrawSamples; else if (blkScale < 1.0) EffectiveMode = ScanSamples; else EffectiveMode = ScanGroups; } // This is the bytes in channels we're not looking at int byteInc = (Channels - 1) * SampleBytes; pDC->Colour(cGrid); pDC->HLine((int)MAX(0,c.x1), (int)MIN(pDC->X(),c.x2), (int)cy); // Setup for drawing bookmarks.. auto &BookmarkArr = Bookmarks[ChannelIdx]; for (auto &bm: BookmarkArr) bm.x = SampleToView(bm.sample); auto nextBookmark = BookmarkArr.Length() ? BookmarkArr.AddressOf(0) : NULL; auto endBookmark = BookmarkArr.Length() ? nextBookmark + BookmarkArr.Length() : NULL; if (EffectiveMode == DrawSamples) { sample_t pSample(start + (((StartSample * Channels) + ChannelIdx) * SampleBytes)); bool DrawDots = SampleRange < (Client.X() >> 2); if (CursorX >= Client.x1 && CursorX <= Client.x2) { pDC->Colour(L_BLACK); pDC->VLine(CursorX, (int)c.y1, (int)c.y2); } while ( nextBookmark && nextBookmark < endBookmark) { pDC->Colour(nextBookmark->colour); pDC->VLine(nextBookmark->x, (int)c.y1, (int)c.y2); nextBookmark++; } // For all samples in the view space... pDC->Colour(cMax); LPoint Prev(-1, -1); for (auto idx = StartSample; idx <= EndSample + 1; idx++, pSample.i8 += byteInc) { auto n = Convert(pSample); auto x = SampleToView(idx); auto y = (cy - ((int64_t)n * YScale / MaxT)); if (idx != StartSample) pDC->Line(Prev.x, Prev.y, (int)x, (int)y); Prev.Set((int)x, (int)y); if (DrawDots) pDC->Rectangle((int)x-1, (int)y-1, (int)x+1, (int)y+1); } } else { // For all x coordinates in the view space.. for (int Vx=Client.x1; Vx<=Client.x2; Vx++) { if (Vx % 100 == 0) { PROF("Pixels"); } if (Vx < c.x1 || Vx > c.x2) continue; if (nextBookmark && nextBookmark->x <= Vx) { while ( nextBookmark < endBookmark && nextBookmark->x <= Vx) { pDC->Colour(nextBookmark->colour); pDC->VLine(nextBookmark->x, (int)c.y1, (int)c.y2); nextBookmark++; } } auto isCursor = CursorX == Vx; if (isCursor) { pDC->Colour(L_BLACK); pDC->VLine(Vx, (int)c.y1, (int)c.y2); } Grp Min, Max; StartSample = ViewToSample(Vx); EndSample = ViewToSample(Vx+1); // printf("SampleIdx: " LPrintfSizeT "-" LPrintfSizeT "\n", StartSample, EndSample); if (EffectiveMode == ScanSamples) { // Scan individual samples sample_t pStart(start + ((StartSample * Channels + ChannelIdx) * SampleBytes)); auto pEnd = start + ((EndSample * Channels + ChannelIdx) * SampleBytes); #if 0 if (Vx==Client.x1) { auto PosX = SampleToView((pStart - ChannelIdx - start) / Channels); LgiTrace(" ptr=%i " LPrintfSizeT "-" LPrintfSizeT " x=%i pos=%i\n", (int)((char*)pStart-(char*)start), StartSample, EndSample, Vx, PosX); } #endif for (auto i = pStart; i.i8 < pEnd; i.i8 += byteInc) { auto n = Convert(i); Max.Min = MIN(Max.Min, n); Max.Max = MAX(Max.Max, n); } } else { // Scan the buckets auto &Graph = (*Grps)[ChannelIdx]; double blkStart = (double)StartSample * Graph.Length() / samples; double blkEnd = (double)EndSample * Graph.Length() / samples; /* if (Vx % 10 == 0) printf("BlkRange[%i]: %g-%g of:%i\n", Vx, blkStart, blkEnd, (int)Graph.Length()); */ #if 0 if (isCursor) LgiTrace("Blk %g-%g of " LPrintfSizeT "\n", blkStart, blkEnd, Graph.Length()); #endif auto iEnd = (int)ceil(blkEnd); int count = 0; for (int i=(int)floor(blkStart); iColour(isCursor ? cCursorMax : cMax); pDC->VLine(Vx, (int)y1, (int)y2); if (EffectiveMode == ScanGroups) { y1 = (cy - (Min.Max * YScale / MaxT)); y2 = (cy - (Min.Min * YScale / MaxT)); pDC->Colour(isCursor ? cCursorMin : cMin); pDC->VLine(Vx, (int)y1, (int)y2); } } } } void OnPaint(LSurface *pDC) override { #ifdef WINDOWS LDoubleBuffer DblBuf(pDC); #endif pDC->Colour(L_WORKSPACE); pDC->Rectangle(); auto Fnt = GetFont(); Fnt->Transparent(true); if (!FilePath) { LDisplayString ds(Fnt, Msg); Fnt->Fore(L_LOW); auto c = GetClient().Center(); ds.Draw(pDC, c.x - (ds.X()/2), c.y - (ds.Y()/2)); return; } if (!Data.Valid()) { auto r = DefaultPos(); SetData(r); } LDisplayString ds(Fnt, ErrorMsg ? ErrorMsg : Msg); Fnt->Fore(ErrorMsg ? LColour::Red : LColour(L_LOW)); ds.Draw(pDC, 4, 4); if (ErrorMsg) return; switch (SampleType) { case AudioS16LE: case AudioS16BE: { for (uint32_t ch = 0; ch < Channels; ch++) PaintSamples(pDC, ch, &IntGrps); break; } case AudioS24LE: case AudioS24BE: { for (uint32_t ch = 0; ch < Channels; ch++) PaintSamples(pDC, ch, &IntGrps); break; } case AudioS32LE: case AudioS32BE: { for (uint32_t ch = 0; ch < Channels; ch++) PaintSamples(pDC, ch, &IntGrps); break; } default: { LAssert(!"Not impl."); break; } } } void TogglePlay() { if (AoPlaying) { LgiTrace("stopping...\n"); AoPlaying = false; } else // Start playback { if (AoDriver < 0) { AoDriver = ao_default_driver_id(); } if (!AoDev) { ao_sample_format fmt; fmt.bits = SampleBits; fmt.rate = SampleRate; fmt.channels = Channels; fmt.byte_format = AO_FMT_LITTLE; fmt.matrix = NULL; AoDev = ao_open_live(AoDriver, &fmt, NULL); } if (AoDev) { AoPlaying = true; AoEvent.Signal(); } } } int Main() override { int TimeSlice = 50; // ms int sliceBytes = 0; int sliceSamples = 0; int64_t playStart = 0; int64_t playSample = 0; bool playedSamples = false; while (!IsCancelled()) { if (AoPlaying) { // Play one timeslice of audio auto addr = AddressOf(CursorSample); /* auto startTs = LMicroTime(); auto elapsedSamples = CursorSample - playSample; while ((LMicroTime() - playStart) < elapsedSamples) ; auto waitTs = LMicroTime() - startTs; */ ao_play(AoDev, (char*)addr.i8, sliceBytes); CursorSample += sliceSamples; playedSamples = true; /* #if 1 LgiTrace("wait %.3fms\n", (double)waitTs / 1000.0); #else auto now = LMicroTime(); auto elapsedTime = now - playStart; double ms = (double)elapsedTime/1000.0; elapsedSamples = CursorSample - playSample; double sms = (double)elapsedSamples * 1000.0 / SampleRate; LgiTrace("play ms=%.3f sms=%.3f\n", ms, sms); #endif */ } else { if (playedSamples) { playedSamples = false; LgiTrace("stopped.\n"); } AoEvent.Wait(100); if (AoPlaying) { sliceSamples = SampleRate * TimeSlice / 1000; sliceBytes = Channels * sliceSamples * (SampleBits / 8); playStart = LMicroTime(); playSample = CursorSample; } } } return 0; } bool UnitTests() { SampleType = AudioS16LE; auto c = GetConvert(); int16_t le16 = (2 << 8) | (1); sample_t ptr(&le16); auto out = c(ptr); if (out != 0x201) { LAssert(0); return false; } SampleType = AudioS16BE; c = GetConvert(); int16_t be16 = (1 << 8) | (2); ptr = &be16; out = c(ptr); if (out != 0x201) { LAssert(0); return false; } SampleType = AudioS24LE; c = GetConvert(); uint8_t le24[] = {1, 2, 3}; ptr = &le24; out = c(ptr); if (out != 0x30201) { LAssert(0); return false; } SampleType = AudioS24BE; c = GetConvert(); uint8_t be24[] = {3, 2, 1}; ptr = &be24; out = c(ptr); if (out != 0x30201) { LAssert(0); return false; } SampleType = AudioS32LE; c = GetConvert(); uint8_t le32[] = {1, 2, 3, 4}; ptr = &le32; out = c(ptr); if (out != 0x4030201) { LAssert(0); return false; } SampleType = AudioS32BE; c = GetConvert(); uint8_t be32[] = {4, 3, 2, 1}; ptr = &be32; out = c(ptr); if (out != 0x4030201) { LAssert(0); return false; } return true; } }; class LAudioRepairView : public LAudioView { LArray> &GetBookMarks() { return Bookmarks; } void RepairAll() { for (int ch = 0; ch < Bookmarks.Length(); ch++) for (auto &bm: Bookmarks[ch]) RepairBookmark(ch, bm); Invalidate(); } void RepairBookmark(int Channel, LBookmark &bm) { if (!bm.error) return; auto Convert = GetConvert(); auto end = Audio.AddressOf(Audio.Length()-1) + 1; int sampleBytes = SampleBits / 8; int skipBytes = (Channels - 1) * sampleBytes; int channelOffset = Channel * sampleBytes; // Look at previous sample... auto repairSample = bm.sample; auto p = AddressOf(repairSample - 1); p.i8 += channelOffset; auto lastGood = Convert(p); p.i8 += skipBytes; // Seek over and find the end... auto nextGood = Convert(p); p.i8 += skipBytes; while (nextGood) { nextGood = Convert(p); p.i8 += skipBytes; repairSample++; } auto endRepair = repairSample; while (nextGood == 0) { nextGood = Convert(p); p.i8 += skipBytes; endRepair++; } int64_t sampleDiff = nextGood - lastGood; int64_t repairSamples = endRepair - repairSample + 1; // Write the new interpolated samples for (int64_t i = repairSample; i < endRepair; i++) { p = AddressOf(i); p.i8 += channelOffset; if (p.i8 >= end) break; switch (SampleType) { case AudioS24LE: { auto off = i - repairSample + 1; p.i24->s = lastGood + (off * sampleDiff / repairSamples); break; } default: { LAssert(!"Impl me."); break; } } } bm.colour = LColour::Green; bm.error = false; } void RepairNext() { for (uint32_t ch = 0; ch < Channels; ch++) { int64_t prev = 0; LBookmark *Bookmark = NULL; for (auto &bm: Bookmarks[ch]) { if (bm.sample >= CursorSample && prev < CursorSample && bm.error) { Bookmark = &bm; break; } } if (Bookmark) RepairBookmark(ch, *Bookmark); } Invalidate(); } struct ErrorThread : public LThread { LAudioRepairView *view; int64_t start, end; int channel; LArray bookmarks; ErrorThread(LAudioRepairView *v, int64_t startSample, int64_t endSample, int ch) : LThread("ErrorThread", v->AddDispatch()) { view = v; start = startSample; end = endSample; channel = ch; Run(); } void OnComplete() { auto BookMarks = view->GetBookMarks(); BookMarks[channel].Add(bookmarks); BookMarks[channel].Sort([](auto a, auto b) { return (int)(a->sample - b->sample); }); view->Invalidate(); LAssert(view->ErrorThreads.HasItem(this)); view->ErrorThreads.Delete(this); if (view->ErrorThreads.Length() == 0) LPopupNotification::Message(view->GetWindow(), "Done."); DeleteOnExit = true; } int Main() { auto Convert = view->GetConvert(); int32_t prev = 0; int sampleBytes = view->SampleBits / 8; int byteInc = (view->Channels - 1) * sampleBytes; int64_t zeroRunStart = -1; auto ptr = view->AddressOf(0); auto pStart = ptr.i8 + (start * view->Channels * sampleBytes) + (channel * sampleBytes); auto pEnd = ptr.i8 + (end * view->Channels * sampleBytes) + (channel * sampleBytes); ptr.i8 = pStart; while (ptr.i8 < pEnd) { auto s = Convert(ptr); ptr.i8 += byteInc; auto diff = s - prev; if (diff < 0) diff = -diff; if (s == 0) { if (zeroRunStart < 0) zeroRunStart = view->PtrToSample(ptr) - 1; } else if (zeroRunStart >= 0) { auto cur = view->PtrToSample(ptr) - 1; auto len = cur - zeroRunStart; if (len >= ERROR_ZERO_SAMPLE_COUNT) { // emit book mark auto &bm = bookmarks.New(); bm.sample = zeroRunStart; bm.colour = LColour::Red; bm.error = true; #if 0 if (Bookmarks.Length() >= 10000) break; #endif } zeroRunStart = -1; } prev = s; } return 0; } }; LArray ErrorThreads; public: void FindErrors() { if (ErrorThreads.Length() > 0) { LPopupNotification::Message(GetWindow(), "Already finding errors."); return; } LPopupNotification::Message(GetWindow(), "Finding errors..."); Bookmarks.Length(Channels); auto samples = GetSamples(); auto threads = LAppInst->GetCpuCount(); auto threadsPerCh = threads / Channels; for (uint32_t ch=0; ch Msgs; LPoint Decor = LPoint(2, 2); uint64_t HideTs = 0; LPoint CalcSize(); public: static LPopupNotification *Message(LWindow *ref, LString msg); LPopupNotification(LWindow *ref, LString msg = LString()); ~LPopupNotification(); void Init(); void Add(LWindow *ref, LString msg); void OnPaint(LSurface *pDC); void OnPulse(); }; diff --git a/include/lgi/common/Window.h b/include/lgi/common/Window.h --- a/include/lgi/common/Window.h +++ b/include/lgi/common/Window.h @@ -1,333 +1,333 @@ #ifndef _LWINDOW_H_ #define _LWINDOW_H_ #include "lgi/common/View.h" /// The available states for a top level window enum LWindowZoom { /// Minimized LZoomMin, /// Restored/Normal LZoomNormal, /// Maximized LZoomMax }; enum LWindowHookType { LNoEvents = 0, /// \sa LWindow::RegisterHook() LMouseEvents = 1, /// \sa LWindow::RegisterHook() LKeyEvents = 2, /// \sa LWindow::RegisterHook() LKeyAndMouseEvents = LMouseEvents | LKeyEvents, }; /// A top level window. class LgiClass LWindow : public LView, // This needs to be second otherwise is causes v-table problems. #ifndef LGI_SDL virtual #endif public LDragDropTarget { friend class BViewRedir; friend class LApp; friend class LView; friend class LButton; friend class LDialog; friend class LWindowPrivate; friend struct LDialogPriv; bool _QuitOnClose; protected: class LWindowPrivate *d; #if WINNATIVE LWindow *_Dialog = NULL; #elif defined(HAIKU) LWindowZoom _PrevZoom = LZoomNormal; #else OsWindow Wnd; void SetDeleteOnClose(bool i); #endif #if defined __GTK_H__ friend class LMenu; friend void lgi_widget_size_allocate(Gtk::GtkWidget *widget, Gtk::GtkAllocation *allocation); Gtk::GtkWidget *_Root, *_VBox, *_MenuBar; void OnGtkDelete(); Gtk::gboolean OnGtkEvent(Gtk::GtkWidget *widget, Gtk::GdkEvent *event); #elif defined(LGI_CARBON) friend pascal OSStatus LgiWindowProc(EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void *inUserData); void _Delete(); bool _RequestClose(bool os); #elif defined(__OBJC__) public: // This returns the root level content NSView NSView *Handle(); protected: #endif /// The default button LViewI *_Default = NULL; /// The menu on the window LMenu *Menu = NULL; void SetChildDialog(LDialog *Dlg); void SetDragHandlers(bool On); /// Haiku: This shuts down the window's thread cleanly. int WaitThread(); public: #ifdef _DEBUG LMemDC DebugDC; #endif #ifdef __GTK_H__ LWindow(Gtk::GtkWidget *w = NULL); #elif LGI_CARBON LWindow(WindowRef wr = NULL); #elif LGI_COCOA LWindow(OsWindow wnd = NULL); #else LWindow(); #endif ~LWindow(); const char *GetClass() override { return "LWindow"; } /// Lays out the child views into the client area. virtual void PourAll(); /// Returns the current menu object LMenu *GetMenu() { return Menu; } /// Set the menu object. void SetMenu(LMenu *m) { Menu = m; } /// Set the window's icon bool SetIcon(const char *FileName); /// Don't show title bar bool SetTitleBar(bool ShowTitleBar); /// Gets the "quit on close" setting. bool GetQuitOnClose() { return _QuitOnClose; } /// \brief Sets the "quit on close" setting. /// /// When this is switched on the application will quit the main message /// loop when this LWindow is closed. This is really useful for your /// main application window. Otherwise the UI will disappear but the /// application is still running. void SetQuitOnClose(bool i) { _QuitOnClose = i; } bool GetSnapToEdge(); void SetSnapToEdge(bool b); bool GetAlwaysOnTop(); void SetAlwaysOnTop(bool b); /// Gets the current zoom setting LWindowZoom GetZoom(); /// Sets the current zoom void SetZoom(LWindowZoom i); /// Raises the window to the top of the stack. void Raise(); /// Moves a top level window on screen. void MoveOnScreen(); /// Moves a top level to the center of the screen void MoveToCenter(); /// Moves a top level window to where the mouse is void MoveToMouse(); /// Moves the window to somewhere on the same screen as 'wnd' bool MoveSameScreen(LViewI *wnd); // Focus setting LViewI *GetFocus(); enum FocusType { GainFocus, LoseFocus, ViewDelete }; void SetFocus(LViewI *ctrl, FocusType type); /// This setting can turn of taking focus when the window is shown. Useful for popups that /// don't want to steal focus from an underlying window. /// The default value is 'true' - void SetWillFocus(bool f); + bool SetWillFocus(bool f); /// Registers a watcher to receive OnView... messages before they /// are passed through to the intended recipient. bool RegisterHook ( /// The target view. LView *Target, /// Combination of: /// #LMouseEvents - Where Target->OnViewMouse(...) is called for each click. /// and /// #LKeyEvents - Where Target->OnViewKey(...) is called for each key. /// OR'd together. LWindowHookType EventType, /// Not implemented int Priority = 0 ); /// Unregisters a hook target bool UnregisterHook(LView *Target); /// Gets the default view LViewI *GetDefault(); /// Sets the default view void SetDefault(LViewI *v); /// Saves/loads the window's state, e.g. position, minimized/maximized etc bool SerializeState ( /// The data store for reading/writing LDom *Store, /// The field name to use for storing settings under const char *FieldName, /// TRUE if loading the settings into the window, FALSE if saving to the store. bool Load ); /// Builds a map of keyboard short cuts. typedef LHashTbl,LViewI*> ShortcutMap; void BuildShortcuts(ShortcutMap &Map, LViewI *v = NULL); ////////////////////// Events /////////////////////////////// /// Called when the window zoom state changes. virtual void OnZoom(LWindowZoom Action) {} /// Called when the tray icon is clicked. (if present) virtual void OnTrayClick(LMouse &m); /// Called when the tray icon menu is about to be displayed. virtual void OnTrayMenu(LSubMenu &m) {} /// Called when the tray icon menu item has been selected. virtual void OnTrayMenuResult(int MenuId) {} /// Called when files are dropped on the window. virtual void OnReceiveFiles(LArray &Files) {} /// Called when a URL is sent to the window virtual void OnUrl(const char *Url) {}; ///////////////// Implementation //////////////////////////// void OnPosChange() override; LMessage::Result OnEvent(LMessage *Msg) override; void OnPaint(LSurface *pDC) override; /// Allow the window to filter mouse events: /// \returns false if the Window consumed the event. bool HandleViewMouse(LView *v, LMouse &m); /// Allow the window to filter key events: /// \returns false if the Window consumed the event. bool HandleViewKey(LView *v, LKey &k); /// Return true to accept application quit bool OnRequestClose(bool OsShuttingDown) override; bool Obscured(); bool Visible() override; void Visible(bool i) override; bool IsActive(); bool SetActive(); LRect &GetPos() override; void SetDecor(bool Visible); LPoint GetDpi(); LPointF GetDpiScale(); void ScaleSizeToDpi(); // D'n'd int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) override; int OnDrop(LArray &Data, LPoint Pt, int KeyState) override; #if !WINNATIVE bool Attach(LViewI *p) override; // Props #if defined(HAIKU) OsWindow WindowHandle() override; #else OsWindow WindowHandle() override { return Wnd; } #endif bool Name(const char *n) override; const char *Name() override; bool SetPos(LRect &p, bool Repaint = false) override; LRect &GetClient(bool InClientSpace = true) override; // Events void OnChildrenChanged(LViewI *Wnd, bool Attaching) override; void OnCreate() override; virtual void OnFrontSwitch(bool b); #else OsWindow WindowHandle() override { return _View; } #endif #if HAIKU void SetModalDialog(LWindow *dlg); #elif defined(LGI_SDL) virtual bool PushWindow(LWindow *v); virtual LWindow *PopWindow(); #elif defined __GTK_H__ void OnGtkRealize(); bool IsAttached(); void Quit(bool DontDelete = false); LRect *GetDecorSize(); bool TranslateMouse(LMouse &m); LViewI *WindowFromPoint(int x, int y, bool Debug = false); void _SetDynamic(bool b); void _OnViewDelete(); void SetParent(LViewI *p) override; #elif defined(MAC) bool PostEvent(int Cmd, LMessage::Param a = 0, LMessage::Param b = 0, int64_t TimeoutMs = -1) override; void Quit(bool DontDelete = false) override; int OnCommand(int Cmd, int Event, OsView Wnd) override; LViewI *WindowFromPoint(int x, int y, int DebugDebug = 0) override; #if defined(LGI_CARBON) OSErr HandlerCallback(DragTrackingMessage *tracking, DragRef theDrag); #endif #endif }; #endif diff --git a/include/lgi/mac/cocoa/LCocoaView.h b/include/lgi/mac/cocoa/LCocoaView.h --- a/include/lgi/mac/cocoa/LCocoaView.h +++ b/include/lgi/mac/cocoa/LCocoaView.h @@ -1,122 +1,123 @@ // // LCocoaView.h // LgiCocoa // // Created by Matthew Allen on 20/10/18. // Copyright © 2018 Memecode. All rights reserved. // #ifndef LCocoaView_h #define LCocoaView_h #if defined __OBJC__ class LViewI; class LWindow; class LWindowPrivate; enum LCloseContext { CloseNone, CloseUser, CloseDestructor, }; LgiExtern LRect LScreenFlip(LRect r); #define objc_dynamic_cast(TYPE, object) \ ({ \ TYPE *dyn_cast_object = (TYPE*)(object); \ [dyn_cast_object isKindOfClass:[TYPE class]] ? dyn_cast_object : nil; \ }) @interface LCocoaMsg : NSObject { } @property LViewI *v; @property int m; @property LMessage::Param a; @property LMessage::Param b; - (id)init:(LViewI*)view msg:(int)Msg a:(LMessage::Param)A b:(LMessage::Param)B; @end @interface LCocoaAssert : NSObject { } @property LString msg; @property NSModalResponse result; - (id)init:(LString)m; @end // This class wraps a Cocoa NSView and redirects all the calls to LGI's LWindow object. // @interface LCocoaView : NSView { struct DndEvent *dnd; } @property LView *w; @property LString WndClass; // Object life time - (id)init:(LView*)wnd; - (void)dealloc; // Painting - (void)drawRect:(NSRect)dirtyRect; // Mouse - (void)mouseDown:(NSEvent*)ev; - (void)mouseUp:(NSEvent*)ev; - (void)rightMouseDown:(NSEvent*)ev; - (void)rightMouseUp:(NSEvent*)ev; - (void)mouseMoved:(NSEvent*)ev; - (void)mouseDragged:(NSEvent*)ev; - (void)scrollWheel:(NSEvent*)ev; // Keyboard - (void)keyDown:(NSEvent*)event; - (void)keyUp:(NSEvent*)event; - (BOOL)acceptsFirstResponder; // Message handling - (void)userEvent:(LCocoaMsg*)ev; // DnD - (NSDragOperation)draggingEntered:(id )sender; - (NSDragOperation)draggingUpdated:(id )sender; - (void)draggingExited:(nullable id )sender; - (BOOL)prepareForDragOperation:(id )sender; - (BOOL)performDragOperation:(id )sender; - (void)concludeDragOperation:(nullable id )sender; - (void)draggingEnded:(nullable id )sender; @end @interface LNsWindow : NSWindow { enum CloseState { CSNone, CSInRequest, CSClosed, }; CloseState ReqClose; } @property LWindowPrivate *d; +@property bool canFocus; - (id)init:(LWindowPrivate*)priv Frame:(NSRect)rc; - (void)dealloc; - (BOOL)canBecomeKeyWindow; - (LWindow*)getWindow; - (void)onQuit; - (void)onDelete:(LCloseContext)ctx; @end #endif #endif /* LCocoaView_h */ diff --git a/src/common/Lgi/AppCommon.cpp b/src/common/Lgi/AppCommon.cpp --- a/src/common/Lgi/AppCommon.cpp +++ b/src/common/Lgi/AppCommon.cpp @@ -1,296 +1,304 @@ #include "lgi/common/Lgi.h" #include "lgi/common/PopupNotification.h" #include "AppPriv.h" #ifdef LINUX namespace Gtk { #include "LgiWidget.h" } #endif class LPopupNotificationFactory { public: LHashTbl, LPopupNotification*> map; void Empty() { while (map.Length()) { auto p = map.begin(); if (p != map.end()) { delete (*p).value; } } } } PopupNotificationFactory; void LApp::CommonCleanup() { PopupNotificationFactory.Empty(); } LString LApp::GetConfigPath() { #if defined(LINUX) ::LFile::Path p(LSP_HOME); p += ".config"; #else ::LFile::Path p(LSP_USER_APP_DATA); p += "MemecodeLgi"; if (!p.Exists()) FileDev->CreateFolder(p); #endif if (p.Exists()) { p += "lgi.json"; return p.GetFull(); } p--; p += ".lgi.json"; return p.GetFull(); } ::LString LApp::GetConfig(const char *Variable) { auto c = d->GetConfig(); return c ? c->Get(Variable) : NULL; } void LApp::SetConfig(const char *Variable, const char *Value) { auto c = d->GetConfig(); if (c && c->Set(Variable, Value)) d->SaveConfig(); } /////////////////////////////////////////////////////////////////////////////////// LJson *LAppPrivate::GetConfig() { if (!Config) { auto Path = Owner->GetConfigPath(); if (Config.Reset(new LJson())) { ::LFile f; if (f.Open(Path, O_READ)) Config->SetJson(f.Read()); bool Dirty = false; #define DEFAULT(var, val) \ if (Config->Get(var).Length() == 0) \ Dirty |= Config->Set(var, val); #ifdef LINUX DEFAULT(LApp::CfgLinuxKeysShift, "GDK_SHIFT_MASK"); DEFAULT(LApp::CfgLinuxKeysCtrl, "GDK_CONTROL_MASK"); DEFAULT(LApp::CfgLinuxKeysAlt, "GDK_MOD1_MASK"); auto Sys = #if defined(MAC) "GDK_MOD2_MASK"; #else "GDK_SUPER_MASK"; #endif DEFAULT(LApp::CfgLinuxKeysSystem, Sys); DEFAULT("Linux.Keys.Debug", "0"); auto str = [](Gtk::GDK_MouseButtons btn) { LString s; s.Printf("%i", btn); return s; }; DEFAULT(LApp::CfgLinuxMouseLeft, str(Gtk::GDK_LEFT_BTN)); DEFAULT(LApp::CfgLinuxMouseMiddle, str(Gtk::GDK_MIDDLE_BTN)); DEFAULT(LApp::CfgLinuxMouseRight, str(Gtk::GDK_RIGHT_BTN)); DEFAULT(LApp::CfgLinuxMouseBack, str(Gtk::GDK_BACK_BTN)); DEFAULT(LApp::CfgLinuxMouseForward, str(Gtk::GDK_FORWARD_BTN)); #endif DEFAULT("Language", "-"); DEFAULT("Fonts.Comment", "Fonts are specified in the : format."); DEFAULT(LApp::CfgFontsGlyphSub, "1"); DEFAULT(LApp::CfgFontsPointSizeOffset, "0"); DEFAULT(LApp::CfgFontsSystemFont, "-"); DEFAULT(LApp::CfgFontsBoldFont, "-"); DEFAULT(LApp::CfgFontsMonoFont, "-"); DEFAULT(LApp::CfgFontsSmallFont, "-"); DEFAULT(LApp::CfgFontsCaptionFont, "-"); DEFAULT(LApp::CfgFontsMenuFont, "-"); DEFAULT("Colours.Comment", "Use CSS hex definitions here, ie #RRGGBB in hex."); #define _(name) \ if (L_##name < L_MAXIMUM) \ DEFAULT("Colours.L_" #name, "-"); _SystemColour(); #undef _ if (Dirty) SaveConfig(); LgiTrace("Using LGI config: '%s'\n", Path.Get()); } else printf("%s:%i - Alloc failed.\n", _FL); } return Config; } bool LAppPrivate::SaveConfig() { auto Path = Owner->GetConfigPath(); if (!Path || !Config) return false; ::LFile f; if (!f.Open(Path, O_WRITE)) return false; f.SetSize(0); return f.Write(Config->GetJson()); } //////////////////////////////////////////////////////////////////////////////////////////////// LPopupNotification *LPopupNotification::Message(LWindow *ref, LString msg) { if (!ref) return NULL; auto w = PopupNotificationFactory.map.Find(ref); if (w) { w->Add(ref, msg); } else { w = new LPopupNotification(ref, msg); if (w) PopupNotificationFactory.map.Add(ref, w); } return w; } LPopupNotification::LPopupNotification(LWindow *ref, LString msg) { - Fore = Back = L_TOOL_TIP; - Fore.ToHLS(); - Fore.SetHLS(Fore.GetH(), Fore.GetL() * 0.4, Fore.GetS()); + cFore = cBack = cBorder = L_TOOL_TIP; + if (cFore.ToHLS()) + { + cBorder.SetHLS(cBorder.GetH(), cBorder.GetL() * 0.6, cBorder.GetS()); + cFore.SetHLS(cFore.GetH(), cFore.GetL() * 0.4, cFore.GetS()); + } + else + { + cBorder = cBorder.Mix(LColour::Black, 0.05f); + cFore = cFore.Mix(LColour::Black, 0.5f); + } // Fore = LColour(0xd4, 0xb8, 0x62); // Back = LColour(0xf7, 0xf0, 0xd5); SetTitleBar(false); SetWillFocus(false); // Don't take focus Name("Notification"); if (ref) RefWnd = ref; if (ref && msg) Add(ref, msg); } LPopupNotification::~LPopupNotification() { LAssert(PopupNotificationFactory.map.Find(RefWnd) == this); PopupNotificationFactory.map.Delete(RefWnd); } LPoint LPopupNotification::CalcSize() { LPoint p; for (auto ds: Msgs) { p.x = MAX(p.x, ds->X()); p.y += ds->Y(); } p.x += Border * 2 + Decor.x; p.y += Border * 2 + Decor.y; return p; } void LPopupNotification::Init() { if (!RefWnd) { LAssert(!"No reference window."); return; } auto r = RefWnd->GetPos(); auto Sz = CalcSize(); LRect pos; pos.ZOff(Sz.x - 1, Sz.y - 1); pos.Offset(r.x2 - pos.X(), r.y2 - pos.Y()); SetPos(pos); SetAlwaysOnTop(true); SetPulse(500); if (!IsAttached()) Attach(0); Visible(true); } void LPopupNotification::Add(LWindow *ref, LString msg) { if (msg) { HideTs = LCurrentTime(); Msgs.Add(new LDisplayString(GetFont(), msg)); } if (ref && RefWnd != ref) RefWnd = ref; if (RefWnd && Msgs.Length() > 0) Init(); } void LPopupNotification::OnPaint(LSurface *pDC) { - pDC->Colour(Fore); + pDC->Colour(cBorder); auto c = GetClient(); pDC->Box(&c); c.Inset(1, 1); - pDC->Colour(Back); + pDC->Colour(cBack); pDC->Rectangle(&c); auto f = GetFont(); - f->Fore(Fore); - f->Back(Back); + f->Fore(cFore); + f->Back(cBack); f->Transparent(true); int x = c.x1 + Border; int y = c.y1 + Border; for (auto ds: Msgs) { ds->Draw(pDC, x, y); y += ds->Y(); } } void LPopupNotification::OnPulse() { if (HideTs && LCurrentTime() >= HideTs + ShowMs) { HideTs = 0; Visible(false); SetPulse(); Msgs.DeleteObjects(); } } diff --git a/src/mac/cocoa/Window.mm b/src/mac/cocoa/Window.mm --- a/src/mac/cocoa/Window.mm +++ b/src/mac/cocoa/Window.mm @@ -1,1436 +1,1455 @@ #include #include "lgi/common/Lgi.h" #include "lgi/common/DragAndDrop.h" #include "lgi/common/Popup.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Menu.h" #include "LCocoaView.h" extern void NextTabStop(LViewI *v, int dir); extern void SetDefaultFocus(LViewI *v); extern void BuildTabStops(LArray &Stops, LViewI *v); #define DEBUG_KEYS 0 #define DEBUG_SETFOCUS 0 #define DEBUG_LOGGING 0 #if DEBUG_LOGGING #define LOG(...) printf(__VA_ARGS__) #else #define LOG(...) #endif /* Deleting a LWindow senarios: Users clicks close: NSWindowDelegate::windowWillClose GWindowPrivate::OnClose(CloseUser) LNsWindow::onDelete Something deletes the LWindow programmatically: LWindow::~LWindow GWindowPriv::OnClose(CloseDestructor) LNsWindow::onDelete self.close windowWillClose -> block Something calls LWindow::Quit() LNsWindow::onQuit (async) self.close NSWindowDelegate::windowWillClose GWindowPrivate::OnClose(CloseUser) LNsWindow::onDelete */ #if DEBUG_SETFOCUS || DEBUG_KEYS -static GString DescribeView(GViewI *v) +static LString DescribeView(LViewI *v) { if (!v) return GString(); char s[512]; int ch = 0; - GArray p; - for (GViewI *i = v; i; i = i->GetParent()) + LArray p; + for (LViewI *i = v; i; i = i->GetParent()) { p.Add(i); } for (int n=MIN(3, (int)p.Length()-1); n>=0; n--) { char Buf[256] = ""; - if (!stricmp(v->GetClass(), "GMdiChild")) + if (!stricmp(v->GetClass(), "LMdiChild")) sprintf(Buf, "'%s'", v->Name()); v = p[n]; ch += sprintf_s(s + ch, sizeof(s) - ch, "%s>%s", Buf, v->GetClass()); } return s; } #endif LRect LScreenFlip(LRect r) { LRect screen(0, 0, -1, -1); for (NSScreen *s in [NSScreen screens]) { LRect pos = s.frame; if (r.Overlap(&pos)) { screen = pos; break; } } if (screen.Valid()) { LRect rc = r; rc.Offset(0, (screen.Y() - r.y1 - r.Y()) - r.y1); // printf("%s:%i - Flip %s -> %s (%s)\n", _FL, r.GetStr(), rc.GetStr(), screen.GetStr()); return rc; } else { // printf("%s:%i - No Screen?\n", _FL); r.ZOff(-1, -1); } return r; } /////////////////////////////////////////////////////////////////////// class HookInfo { public: int Flags; LView *Target; }; @interface LWindowDelegate : NSObject { } - (id)init; - (void)dealloc; - (void)windowDidResize:(NSNotification*)aNotification; - (void)windowDidMove:(NSNotification*)aNotification; - (void)windowWillClose:(NSNotification*)aNotification; - (BOOL)windowShouldClose:(id)sender; - (void)windowDidBecomeMain:(NSNotification*)notification; - (void)windowDidResignMain:(NSNotification*)notification; @end LWindowDelegate *Delegate = nil; class LWindowPrivate { public: - LWindow *Wnd; - LDialog *ChildDlg; - LMenu *EmptyMenu; - LViewI *Focus; - NSView *ContentCache; + LWindow *Wnd = NULL; + LDialog *ChildDlg = NULL; + LMenu *EmptyMenu = NULL; + LViewI *Focus = NULL; + NSView *ContentCache = NULL; - int Sx, Sy; + int Sx = -1, Sy = -1; LKey LastKey; LArray Hooks; - uint64 LastMinimize; - uint64 LastDragDrop; + uint64 LastMinimize = 0; + uint64 LastDragDrop = 0; - bool DeleteOnClose; - bool SnapToEdge; - bool InitVisible; + bool DeleteOnClose = true; + bool SnapToEdge = false; + bool InitVisible = false; LWindowPrivate(LWindow *wnd) { - ContentCache = NULL; - Focus = NULL; - InitVisible = false; - LastMinimize = 0; Wnd = wnd; - LastDragDrop = 0; - DeleteOnClose = true; - ChildDlg = 0; - Sx = Sy = -1; - SnapToEdge = false; - EmptyMenu = 0; } ~LWindowPrivate() { DeleteObj(EmptyMenu); } void OnClose(LCloseContext Ctx) { - LOG("GWindowPrivate::OnClose %p/%s\n", Wnd, Wnd?Wnd->GetClass():NULL); + LOG("LWindowPrivate::OnClose %p/%s\n", Wnd, Wnd?Wnd->GetClass():NULL); auto &osw = Wnd->Wnd; if (!osw) return; LCocoaView *cv = objc_dynamic_cast(LCocoaView, osw.p.contentView); if (cv) cv.w = NULL; LNsWindow *w = objc_dynamic_cast(LNsWindow, osw.p); if (w) [w onDelete:Ctx]; osw.p.delegate = nil; [osw.p autorelease]; osw = nil; if (DeleteOnClose) delete Wnd; } ssize_t GetHookIndex(LView *Target, bool Create = false) { for (int i=0; iTarget = Target; n->Flags = 0; return Hooks.Length() - 1; } } return -1; } void OnResize() { NSWindow *wnd = Wnd->WindowHandle().p; Wnd->Pos = wnd.frame; Wnd->OnPosChange(); wnd.contentView.needsLayout = YES; } }; +#define DefaultStyleMask NSWindowStyleMaskTitled | \ + NSWindowStyleMaskResizable | \ + NSWindowStyleMaskClosable | \ + NSWindowStyleMaskMiniaturizable + @implementation LNsWindow - (id)init:(LWindowPrivate*)priv Frame:(NSRect)rc { - NSUInteger windowStyleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | - NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + NSUInteger windowStyleMask = DefaultStyleMask; if ((self = [super initWithContentRect:rc styleMask:windowStyleMask backing:NSBackingStoreBuffered defer:NO ]) != nil) { self.d = priv; self->ReqClose = CSNone; self.contentView = [[LCocoaView alloc] init:priv->Wnd]; [self makeFirstResponder:self.contentView]; self.acceptsMouseMovedEvents = true; self.ignoresMouseEvents = false; + self.canFocus = true; // printf("LNsWindow.init\n"); } return self; } - (void)dealloc { if (self.d) self.d->Wnd->OnDealloc(); LCocoaView *cv = objc_dynamic_cast(LCocoaView, self.contentView); cv.w = NULL; [cv release]; self.contentView = NULL; + self.canFocus = true; [super dealloc]; // printf("LNsWindow.dealloc.\n"); } - (LWindow*)getWindow { return self.d ? self.d->Wnd : nil; } - (BOOL)canBecomeKeyWindow { - return YES; + return self.canFocus; } - (void)onQuit { #if DEBUG_LOGGING LWindow *wnd = self.d ? self.d->Wnd : NULL; auto cls = wnd ? wnd->GetClass() : NULL; #endif LOG("LNsWindow::onQuit %p/%s %i\n", wnd, cls, self->ReqClose); if (self->ReqClose == CSNone) { self->ReqClose = CSInRequest; if (!self.d) LOG("%s:%i - No priv pointer?\n", _FL); if (!self.d || !self.d->Wnd || !self.d->Wnd->OnRequestClose(false)) { LOG(" ::onQuit %p/%s no 'd' or OnReqClose failed\n", wnd, cls); self->ReqClose = CSNone; return; } } else return; LOG(" ::onQuit %p/%s self.close\n", wnd, cls); self->ReqClose = CSClosed; self.d->Wnd->SetPulse(); [self close]; } - (void)onDelete:(LCloseContext)ctx { LOG("LNsWindow::onDelete %p/%s\n", self.d->Wnd, self.d->Wnd->GetClass()); if (ctx == CloseDestructor && self->ReqClose != CSClosed) { // This is called during the ~LWindow destructor to make sure we // closed the window self->ReqClose = CSClosed; LOG(" ::onDelete %p self.close\n", self.d->Wnd); [self close]; } self.d = NULL; } @end @implementation LWindowDelegate - (id)init { if ((self = [super init]) != nil) { } return self; } - (void)dealloc { [super dealloc]; } - (void)windowDidResize:(NSNotification*)event { LNsWindow *w = event.object; if (w && w.d) w.d->OnResize(); } - (void)windowDidMove:(NSNotification*)event { // LNsWindow *w = event.object; // GRect r = LScreenFlip(w.frame); // printf("windowDidMove: %s\n", r.GetStr()); } - (BOOL)windowShouldClose:(NSWindow*)sender { LNsWindow *w = objc_dynamic_cast(LNsWindow, sender); if (w && w.d && w.d->Wnd) return w.d->Wnd->OnRequestClose(false); return YES; } - (void)windowWillClose:(NSNotification*)event { LNsWindow *w = event.object; if (w && w.d) w.d->OnClose(CloseUser); } - (void)windowDidBecomeMain:(NSNotification*)event { LNsWindow *w = event.object; if (w && w.d) w.d->Wnd->OnFrontSwitch(true); } - (void)windowDidResignMain:(NSNotification*)event { LNsWindow *w = event.object; if (w && w.d) w.d->Wnd->OnFrontSwitch(false); } @end /////////////////////////////////////////////////////////////////////// #define GWND_CREATE 0x0010000 #if __has_feature(objc_arc) #error "NO ARC!" #endif LWindow::LWindow(OsWindow wnd) : LView(NULL) { d = new LWindowPrivate(this); _QuitOnClose = false; Wnd = NULL; Menu = 0; _Default = 0; _Window = this; WndFlags |= GWND_CREATE; LView::Visible(false); _Lock = new LMutex("LWindow"); LRect pos(200, 200, 200, 200); NSRect frame = pos; if (wnd) Wnd = wnd; else Wnd.p = [[LNsWindow alloc] init:d Frame:frame]; if (Wnd) { [Wnd.p retain]; if (!Delegate) Delegate = [[LWindowDelegate alloc] init]; //[Wnd.p makeKeyAndOrderFront:NSApp]; Wnd.p.delegate = Delegate; d->ContentCache = Wnd.p.contentView; } } LWindow::~LWindow() { LOG("LWindow::~LWindow %p\n", this); if (LAppInst->AppWnd == this) LAppInst->AppWnd = 0; _Delete(); d->DeleteOnClose = false; // We're already in the destructor, don't redelete. d->OnClose(CloseDestructor); DeleteObj(Menu); DeleteObj(d); DeleteObj(_Lock); } NSView *LWindow::Handle() { if (!InThread()) return d->ContentCache; if (Wnd.p != nil) return Wnd.p.contentView; return NULL; } bool LWindow::SetTitleBar(bool ShowTitleBar) { - #warning "Impl LWindow::SetTitleBar" - return false; + if (!Wnd.p) + return false; + + if (ShowTitleBar) + { + Wnd.p.titlebarAppearsTransparent = false; + Wnd.p.titleVisibility = NSWindowTitleVisible; + Wnd.p.styleMask = DefaultStyleMask; + } + else + { + Wnd.p.titlebarAppearsTransparent = true; + Wnd.p.titleVisibility = NSWindowTitleHidden; + Wnd.p.styleMask = 0; + } + + return true; } bool LWindow::SetIcon(const char *FileName) { #warning "Impl LWindow::SetIcon" return false; } -void LWindow::SetWillFocus(bool f) +bool LWindow::SetWillFocus(bool f) { - #warning "Impl LWindow::SetWillFocus" + if (!Wnd.p) + return false; + + LNsWindow *w = objc_dynamic_cast(LNsWindow, Wnd.p); + if (!w) + return false; + + w.canFocus = f; + return true; } LViewI *LWindow::GetFocus() { return d->Focus; } void LWindow::SetFocus(LViewI *ctrl, FocusType type) { const char *TypeName = NULL; switch (type) { case GainFocus: TypeName = "Gain"; break; case LoseFocus: TypeName = "Lose"; break; case ViewDelete: TypeName = "Delete"; break; } switch (type) { case GainFocus: { // Check if the control already has focus if (d->Focus == ctrl) return; if (d->Focus) { LView *v = d->Focus->GetGView(); if (v) v->WndFlags &= ~GWF_FOCUS; d->Focus->OnFocus(false); d->Focus->Invalidate(); #if DEBUG_SETFOCUS auto _foc = DescribeView(d->Focus); LgiTrace(".....defocus: %s\n", _foc.Get()); #endif } d->Focus = ctrl; if (d->Focus) { LView *v = d->Focus->GetGView(); if (v) v->WndFlags |= GWF_FOCUS; d->Focus->OnFocus(true); d->Focus->Invalidate(); #if DEBUG_SETFOCUS auto _set = DescribeView(d->Focus); LgiTrace("LWindow::SetFocus(%s, %s) focusing\n", _set.Get(), TypeName); #endif } break; } case LoseFocus: { if (ctrl == d->Focus) { LView *v = d->Focus->GetGView(); if (v) { if (v->WndFlags & GWF_FOCUS) { // View thinks it has focus v->WndFlags &= ~GWF_FOCUS; d->Focus->OnFocus(false); // keep d->Focus pointer, as we want to be able to re-focus the child // view when we get focus again #if DEBUG_SETFOCUS auto _ctrl = DescribeView(ctrl); auto _foc = DescribeView(d->Focus); LgiTrace("LWindow::SetFocus(%s, %s) keep_focus: %s\n", _ctrl.Get(), TypeName, _foc.Get()); #endif } // else view doesn't think it has focus anyway... } else { // Non GView handler d->Focus->OnFocus(false); d->Focus->Invalidate(); d->Focus = NULL; } } else { /* LgiTrace("LWindow::SetFocus(%p.%s, %s) error on losefocus: %p(%s)\n", ctrl, ctrl ? ctrl->GetClass() : NULL, TypeName, d->Focus, d->Focus ? d->Focus->GetClass() : NULL); */ } break; } case ViewDelete: { if (ctrl == d->Focus) { #if DEBUG_SETFOCUS LgiTrace("LWindow::SetFocus(%p.%s, %s) delete_focus: %p(%s)\n", ctrl, ctrl ? ctrl->GetClass() : NULL, TypeName, d->Focus, d->Focus ? d->Focus->GetClass() : NULL); #endif d->Focus = NULL; } break; } } } void LWindow::SetDragHandlers(bool On) { #if 0 if (Wnd && _View) SetAutomaticControlDragTrackingEnabledForWindow(Wnd, On); #endif } void LWindow::Quit(bool DontDelete) { // LAutoPool Pool; if (_QuitOnClose) { _QuitOnClose = false; LCloseApp(); } if (Wnd) SetDragHandlers(false); if (d && DontDelete) { // If DontDelete is true, we should be already in the destructor of the LWindow. // Which means we DON'T call onQuit, as it's too late to ask the user if they don't // want to close the window. The window IS closed come what may, and the object is // going away. Futhermore we can't access the window's memory after it's deleted and // that may happen if the onQuit is processed after ~LWindow. d->DeleteOnClose = false; if (Wnd) [Wnd.p close]; } else if (Wnd) { [Wnd.p performSelectorOnMainThread:@selector(onQuit) withObject:nil waitUntilDone:false]; } } void LWindow::SetChildDialog(LDialog *Dlg) { d->ChildDlg = Dlg; } bool LWindow::GetSnapToEdge() { return d->SnapToEdge; } void LWindow::SetSnapToEdge(bool s) { d->SnapToEdge = s; } void LWindow::OnFrontSwitch(bool b) { if (b && Menu) { [NSApplication sharedApplication].mainMenu = Menu->Handle().p; } else { auto m = LAppInst->Default.Get(); [NSApplication sharedApplication].mainMenu = m ? m->Handle() : nil; } // printf("%s:%i - menu for %s is %p\n", _FL, Name(), [NSApplication sharedApplication].mainMenu); } bool LWindow::Visible() { // LAutoPool Pool; if (!Wnd) return false; return [Wnd.p isVisible]; } void LWindow::Visible(bool i) { // LAutoPool Pool; if (!Wnd) return; if (i) { d->InitVisible = true; PourAll(); [Wnd.p makeKeyAndOrderFront:NULL]; [NSApp activateIgnoringOtherApps:YES]; SetDefaultFocus(this); OnPosChange(); } else { [Wnd.p orderOut:Wnd.p]; } } bool LWindow::IsActive() { return Wnd ? [Wnd.p isKeyWindow] : false; } bool LWindow::SetActive() { [[NSApplication sharedApplication] activateIgnoringOtherApps : YES]; return false; } void LWindow::SetDeleteOnClose(bool i) { d->DeleteOnClose = i; } void LWindow::SetAlwaysOnTop(bool b) { } bool LWindow::PostEvent(int Event, LMessage::Param a, LMessage::Param b, int64_t TimeoutMs) { return LAppInst->PostEvent(this, Event, a, b); } bool LWindow::Attach(LViewI *p) { bool Status = false; if (Wnd) { if (LBase::Name()) Name(LBase::Name()); Status = true; // Setup default button... if (!_Default) { _Default = FindControl(IDOK); if (_Default) _Default->Invalidate(); } OnCreate(); OnAttach(); OnPosChange(); // Set the first control as the focus... NextTabStop(this, 0); } return Status; } bool LWindow::OnRequestClose(bool OsShuttingDown) { if (GetQuitOnClose()) { LCloseApp(); } return LView::OnRequestClose(OsShuttingDown); } bool LWindow::HandleViewMouse(LView *v, LMouse &m) { if (m.Down()) { bool ParentPopup = false; LViewI *p = m.Target; while (p && p->GetParent()) { if (dynamic_cast(p)) { ParentPopup = true; break; } p = p->GetParent(); } if (!ParentPopup) { for (int i=0; iVisible()) { // printf("Hiding popup %s\n", pu->GetClass()); pu->Visible(false); } } } if (!m.IsMove() && LAppInst) { auto mh = LAppInst->GetMouseHook(); if (mh) mh->TrackClick(v); } } for (int i=0; iHooks.Length(); i++) { if (d->Hooks[i].Flags & LMouseEvents) { if (!d->Hooks[i].Target->OnViewMouse(v, m)) { return false; } } } return true; } bool LWindow::HandleViewKey(LView *v, LKey &k) { bool Status = false; LViewI *Ctrl = NULL; if (!v && d->Focus) v = d->Focus->GetGView(); if (!v) { #if DEBUG_KEYS k.Trace("No focus view to handle key."); #endif return false; } // Give key to popups if (LAppInst && LAppInst->GetMouseHook() && LAppInst->GetMouseHook()->OnViewKey(v, k)) { goto AllDone; } // Allow any hooks to see the key... for (int i=0; iHooks.Length(); i++) { if (d->Hooks[i].Flags & LKeyEvents) { if (d->Hooks[i].Target->OnViewKey(v, k)) { Status = true; #if DEBUG_KEYS printf("Hook ate '%c'(%i) down=%i alt=%i ctrl=%i sh=%i\n", k.c16, k.c16, k.Down(), k.Alt(), k.Ctrl(), k.Shift()); #endif goto AllDone; } } } // Give the key to the window... if (v->OnKey(k)) { #if DEBUG_KEYS GString vv = DescribeView(v); printf("%s ate '%c'(%i) down=%i alt=%i ctrl=%i sh=%i\n", vv.Get(), k.c16, k.c16, k.Down(), k.Alt(), k.Ctrl(), k.Shift()); #endif Status = true; goto AllDone; } // Window didn't want the key... switch (k.vkey) { case LK_RETURN: case LK_KEYPADENTER: { Ctrl = _Default; break; } case LK_ESCAPE: { Ctrl = FindControl(IDCANCEL); break; } case LK_TAB: { // Go to the next control? if (k.Down()) { LArray Stops; BuildTabStops(Stops, v->GetWindow()); ssize_t Idx = Stops.IndexOf(v); if (Idx >= 0) { if (k.Shift()) { Idx--; if (Idx < 0) Idx = Stops.Length() - 1; } else { Idx++; if (Idx >= Stops.Length()) Idx = 0; } Stops[Idx]->Focus(true); } } return true; } } if (Ctrl && Ctrl->Enabled()) { if (Ctrl->OnKey(k)) { Status = true; #if DEBUG_KEYS printf("Default Button ate '%c'(%i) down=%i alt=%i ctrl=%i sh=%i\n", k.c16, k.c16, k.Down(), k.Alt(), k.Ctrl(), k.Shift()); #endif goto AllDone; } } if (Menu) { Status = Menu->OnKey(v, k); if (Status) { #if DEBUG_KEYS printf("Menu ate '%c' down=%i alt=%i ctrl=%i sh=%i\n", k.c16, k.Down(), k.Alt(), k.Ctrl(), k.Shift()); #endif } } // Command+W closes the window... if it doesn't get nabbed earlier. if (k.Down() && k.System() && tolower(k.c16) == 'w') { // Close Quit(); return true; } AllDone: if (d) d->LastKey = k; else LAssert(!"Window was deleted and we are accessing unallocated mem."); return Status; } void LWindow::Raise() { if (Wnd) { // BringToFront(Wnd); } } LWindowZoom LWindow::GetZoom() { if (Wnd) { #if 0 bool c = IsWindowCollapsed(Wnd); // printf("IsWindowCollapsed=%i\n", c); if (c) return LZoomMin; c = IsWindowInStandardState(Wnd, NULL, NULL); // printf("IsWindowInStandardState=%i\n", c); if (!c) return LZoomMax; #endif } return LZoomNormal; } void LWindow::SetZoom(LWindowZoom i) { #if 0 OSStatus e = 0; switch (i) { case LZoomMin: { e = CollapseWindow(Wnd, true); if (e) printf("%s:%i - CollapseWindow failed with %i\n", _FL, (int)e); // else printf("LZoomMin ok.\n"); break; } default: case LZoomNormal: { e = CollapseWindow(Wnd, false); if (e) printf("%s:%i - [Un]CollapseWindow failed with %i\n", _FL, (int)e); // else printf("LZoomNormal ok.\n"); break; } } #endif } LViewI *LWindow::GetDefault() { return _Default; } void LWindow::SetDefault(LViewI *v) { if (v && v->GetWindow() == (LViewI*)this) { if (_Default != v) { auto Old = _Default; _Default = v; if (Old) Old->Invalidate(); if (_Default) _Default->Invalidate(); } } else { _Default = 0; } } bool LWindow::Name(const char *n) { // LAutoPool Pool; bool Status = LBase::Name(n); if (Wnd) { NSString *ns = [NSString stringWithCString:n encoding:NSUTF8StringEncoding]; Wnd.p.title = ns; //[ns release]; } return Status; } const char *LWindow::Name() { return LBase::Name(); } LRect &LWindow::GetClient(bool ClientSpace) { // LAutoPool Pool; static LRect r; if (Wnd) { r = Wnd.p.contentView.frame; if (ClientSpace) r.Offset(-r.x1, -r.y1); } else { r.ZOff(-1, -1); } return r; } bool LWindow::SerializeState(LDom *Store, const char *FieldName, bool Load) { if (!Store || !FieldName) return false; if (Load) { LVariant v; if (Store->GetValue(FieldName, v) && v.Str()) { LRect Position(0, 0, -1, -1); LWindowZoom State = LZoomNormal; auto t = LString(v.Str()).SplitDelimit(";"); for (auto s: t) { auto v = s.SplitDelimit("=", 1); if (v.Length() == 2) { if (v[0].Equals("State")) State = (LWindowZoom)v[1].Int(); else if (v[0].Equals("Pos")) Position.SetStr(v[1]); } else return false; } if (Position.Valid()) { if (Position.X() < 64) Position.x2 = Position.x1 + 63; if (Position.Y() < 64) Position.y2 = Position.y1 + 63; SetPos(Position); } SetZoom(State); } else return false; } else { char s[256]; LWindowZoom State = GetZoom(); sprintf(s, "State=%i;Pos=%s", State, GetPos().GetStr()); LVariant v = s; if (!Store->SetValue(FieldName, v)) return false; } return true; } LPoint LWindow::GetDpi() { return LScreenDpi(); } LPointF LWindow::GetDpiScale() { auto Dpi = GetDpi(); return LPointF(Dpi.x / 100.0, Dpi.y / 100.0); } LRect &LWindow::GetPos() { // LAutoPool Pool; if (Wnd) { Pos = LScreenFlip(Wnd.p.frame); // printf("%s::GetPos %s\n", GetClass(), Pos.GetStr()); } return Pos; } bool LWindow::SetPos(LRect &p, bool Repaint) { // LAutoPool Pool; Pos = p; if (Wnd) { LRect r = LScreenFlip(p); [Wnd.p setFrame:r display:YES]; // printf("%s::SetPos %s\n", GetClass(), Pos.GetStr()); } return true; } void LWindow::OnChildrenChanged(LViewI *Wnd, bool Attaching) { if (dynamic_cast(Wnd)) { printf("%s:%i - Ignoring GPopup in OnChildrenChanged handler.\n", _FL); return; } PourAll(); } void LWindow::OnCreate() { } void LWindow::OnPaint(LSurface *pDC) { pDC->Colour(L_MED); pDC->Rectangle(); } void LWindow::OnPosChange() { LView::OnPosChange(); if (d->Sx != X() || d->Sy != Y()) { PourAll(); d->Sx = X(); d->Sy = Y(); } } #define IsTool(v) \ ( \ dynamic_cast(v) \ && \ dynamic_cast(v)->_IsToolBar \ ) void LWindow::PourAll() { LRect r = GetClient(); // printf("::Pour r=%s\n", r.GetStr()); LRegion Client(r); LRegion Update(Client); bool HasTools = false; LViewI *v; List::I Lst = Children.begin(); { LRegion Tools; for (v = *Lst; v; v = *++Lst) { if (IsTool(v)) { LRect OldPos = v->GetPos(); Update.Union(&OldPos); if (HasTools) { // 2nd and later toolbars if (v->Pour(Tools)) { if (!v->Visible()) { v->Visible(true); } if (OldPos != v->GetPos()) { // position has changed update... v->Invalidate(); } Tools.Subtract(&v->GetPos()); Update.Subtract(&v->GetPos()); } } else { // First toolbar if (v->Pour(Client)) { HasTools = true; if (!v->Visible()) { v->Visible(true); } if (OldPos != v->GetPos()) { v->Invalidate(); } LRect Bar(v->GetPos()); Bar.x2 = GetClient().x2; Tools = Bar; Tools.Subtract(&v->GetPos()); Client.Subtract(&Bar); Update.Subtract(&Bar); } } } } } Lst = Children.begin(); for (LViewI *v = *Lst; v; v = *++Lst) { if (!IsTool(v)) { LRect OldPos = v->GetPos(); Update.Union(&OldPos); if (v->Pour(Client)) { if (!v->Visible()) { v->Visible(true); } v->Invalidate(); Client.Subtract(&v->GetPos()); Update.Subtract(&v->GetPos()); } else { // non-pourable } } } for (int i=0; iMsg()) { case M_CLOSE: { if (Wnd) [Wnd.p performSelectorOnMainThread:@selector(onQuit) withObject:nil waitUntilDone:false]; else LAssert(!"No window?"); break; } case M_DESTROY: { delete this; return true; } } return LView::OnEvent(m); } bool LWindow::RegisterHook(LView *Target, LWindowHookType EventType, int Priority) { bool Status = false; if (Target && EventType) { ssize_t i = d->GetHookIndex(Target, true); if (i >= 0) { d->Hooks[i].Flags = EventType; Status = true; } } return Status; } bool LWindow::UnregisterHook(LView *Target) { ssize_t i = d->GetHookIndex(Target); if (i >= 0) { d->Hooks.DeleteAt(i); return true; } return false; } LViewI *LWindow::WindowFromPoint(int x, int y, int DebugDepth) { for (int i=0; iVisible()) { auto r = p->GetPos(); if (r.Overlap(x, y)) { // printf("WindowFromPoint got %s click (%i,%i)\n", p->GetClass(), x, y); return p->WindowFromPoint(x - r.x1, y - r.y1, DebugDepth ? DebugDepth + 1 : 0); } } } return LView::WindowFromPoint(x, y, DebugDepth ? DebugDepth + 1 : 0); } int LWindow::OnCommand(int Cmd, int Event, OsView SrcCtrl) { #if 0 OsView v; switch (Cmd) { case kHICommandCut: { OSErr e = GetKeyboardFocus(Wnd, (ControlRef*) &v); if (!e) LgiPostEvent(v, M_CUT); break; } case kHICommandCopy: { OSErr e = GetKeyboardFocus(Wnd, (ControlRef*) &v); if (!e) LgiPostEvent(v, M_COPY); break; } case kHICommandPaste: { OSErr e = GetKeyboardFocus(Wnd, (ControlRef*) &v); if (!e) LgiPostEvent(v, M_PASTE); break; } case 'dele': { OSErr e = GetKeyboardFocus(Wnd, (ControlRef*) &v); if (!e) LgiPostEvent(v, M_DELETE); break; } } #endif return 0; } void LWindow::OnTrayClick(LMouse &m) { if (m.Down() || m.IsContextMenu()) { LSubMenu RClick; OnTrayMenu(RClick); if (GetMouse(m, true)) { #if WINNATIVE SetForegroundWindow(Handle()); #endif int Result = RClick.Float(this, m.x, m.y); #if WINNATIVE PostMessage(Handle(), WM_NULL, 0, 0); #endif OnTrayMenuResult(Result); } } } bool LWindow::Obscured() { // LAutoPool Pool; if (!Wnd) return false; auto s = [Wnd.p occlusionState]; return !(s & NSWindowOcclusionStateVisible); }